• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.compatibility.common.tradefed.targetprep;
17 
18 import static com.android.tradefed.targetprep.UserHelper.getRunTestsAsUser;
19 
20 import com.android.annotations.VisibleForTesting;
21 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
22 import com.android.compatibility.common.util.DynamicConfig;
23 import com.android.compatibility.common.util.DynamicConfigHandler;
24 import com.android.compatibility.common.util.UrlReplacement;
25 import com.android.tradefed.dependencies.ExternalDependency;
26 import com.android.tradefed.dependencies.IExternalDependency;
27 import com.android.tradefed.dependencies.connectivity.NetworkDependency;
28 import com.android.tradefed.build.IBuildInfo;
29 import com.android.tradefed.config.Option;
30 import com.android.tradefed.config.OptionClass;
31 import com.android.tradefed.device.DeviceNotAvailableException;
32 import com.android.tradefed.device.ITestDevice;
33 import com.android.tradefed.device.NativeDevice;
34 import com.android.tradefed.device.contentprovider.ContentProviderHandler;
35 import com.android.tradefed.invoker.IInvocationContext;
36 import com.android.tradefed.invoker.TestInformation;
37 import com.android.tradefed.log.LogUtil.CLog;
38 import com.android.tradefed.result.error.DeviceErrorIdentifier;
39 import com.android.tradefed.result.error.InfraErrorIdentifier;
40 import com.android.tradefed.targetprep.BaseTargetPreparer;
41 import com.android.tradefed.targetprep.BuildError;
42 import com.android.tradefed.targetprep.TargetSetupError;
43 import com.android.tradefed.testtype.IInvocationContextReceiver;
44 import com.android.tradefed.testtype.suite.TestSuiteInfo;
45 import com.android.tradefed.util.FileUtil;
46 import com.android.tradefed.util.StreamUtil;
47 
48 import org.json.JSONException;
49 import org.xmlpull.v1.XmlPullParserException;
50 
51 import java.io.File;
52 import java.io.FileNotFoundException;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.net.URL;
56 import java.util.HashSet;
57 import java.util.List;
58 import java.util.Set;
59 
60 /** Pushes dynamic config files from config repository */
61 @OptionClass(alias = "dynamic-config-pusher")
62 public class DynamicConfigPusher extends BaseTargetPreparer
63         implements IInvocationContextReceiver, IExternalDependency {
64     public enum TestTarget {
65         DEVICE,
66         HOST
67     }
68 
69     /* API Key for compatibility test project, used for dynamic configuration. */
70     private static final String API_KEY = "AIzaSyAbwX5JRlmsLeygY2WWihpIJPXFLueOQ3U";
71 
72     @Option(name = "api-key", description = "API key for for dynamic configuration.")
73     private String mApiKey = API_KEY;
74 
75     @Option(name = "cleanup", description = "Whether to remove config files from the test " +
76             "target after test completion.")
77     private boolean mCleanup = true;
78 
79     @Option(name = "config-url", description = "The url path of the dynamic config. If set, " +
80             "will override the default config location defined in CompatibilityBuildProvider.")
81     private String mConfigUrl = "https://androidpartner.googleapis.com/v1/dynamicconfig/" +
82             "suites/{suite-name}/modules/{module}/version/{version}?key={api-key}";
83 
84     @Option(
85             name = "has-server-side-config",
86             description = "Whether there exists a service side dynamic config.")
87     private boolean mHasServerSideConfig = true;
88 
89     @Option(name="config-filename", description = "The module name for module-level " +
90             "configurations, or the suite name for suite-level configurations")
91     private String mModuleName = null;
92 
93     @Option(name = "target", description = "The test target, \"device\" or \"host\"",
94             mandatory = true)
95     private TestTarget mTarget;
96 
97     @Option(name = "version", description = "The version of the configuration to retrieve " +
98             "from the server, e.g. \"1.0\". Defaults to suite version string.")
99     private String mVersion;
100 
101     // Options for getting the dynamic file from resources.
102     @Option(
103             name = "extract-from-resource",
104             description =
105                     "Whether to look for the local dynamic config inside the jar resources "
106                             + "or on the local disk.")
107     private boolean mExtractFromResource = false;
108 
109     @Option(
110             name = "dynamic-resource-name",
111             description =
112                     "When using --extract-from-resource, this option allow to specify the resource"
113                             + " name, instead of the module name for the lookup. File will still be"
114                             + " logged under the module name.")
115     private String mResourceFileName = null;
116 
117     @Option(
118             name = "dynamic-config-name",
119             description =
120                     "The dynamic config name for module-level configurations, or the "
121                             + "suite name for suite-level configurations.")
122     private String mDynamicConfigName = null;
123 
124     private String mDeviceFilePushed;
125 
126     private IInvocationContext mModuleContext = null;
127 
setModuleName(String moduleName)128     public void setModuleName(String moduleName) {
129         mModuleName = moduleName;
130     }
131 
132     /** {@inheritDoc} */
133     @Override
setInvocationContext(IInvocationContext invocationContext)134     public void setInvocationContext(IInvocationContext invocationContext) {
135         mModuleContext = invocationContext;
136     }
137 
138     /** {@inheritDoc} */
139     @Override
getDependencies()140     public Set<ExternalDependency> getDependencies() {
141         Set<ExternalDependency> dependencies = new HashSet<>();
142         dependencies.add(new NetworkDependency());
143         return dependencies;
144     }
145 
146     /** {@inheritDoc} */
147     @Override
setUp(TestInformation testInfo)148     public void setUp(TestInformation testInfo)
149             throws TargetSetupError, BuildError, DeviceNotAvailableException {
150         UrlReplacement.init();
151         IBuildInfo buildInfo = testInfo.getBuildInfo();
152         ITestDevice device = testInfo.getDevice();
153         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
154 
155         File localConfigFile = getLocalConfigFile(buildHelper, device);
156 
157         String suiteName =
158                 (mModuleContext != null) ? getSuiteName() : TestSuiteInfo.getInstance().getName();
159         // Ensure mModuleName is set.
160         if (mModuleName == null) {
161             mModuleName = suiteName.toLowerCase();
162             CLog.w("Option config-filename isn't set. Using suite-name '%s'", mModuleName);
163             if (buildHelper.getDynamicConfigFiles().get(mModuleName) != null) {
164                 CLog.i("Dynamic config file already collected, skipping DynamicConfigPusher.");
165                 return;
166             }
167         }
168         if (mVersion == null) {
169             mVersion = buildHelper.getSuiteVersion();
170         }
171 
172         String apfeConfigInJson = resolveUrl(suiteName);
173         // Use DynamicConfigHandler to merge local and service configuration into one file
174         File hostFile = mergeConfigFiles(localConfigFile, apfeConfigInJson, mModuleName, device);
175 
176         if (TestTarget.DEVICE.equals(mTarget)) {
177             String deviceDest =
178                     String.format(
179                             "%s%s.dynamic",
180                             DynamicConfig.CONFIG_FOLDER_ON_DEVICE, createModuleName());
181             int userId = getRunTestsAsUser(testInfo);
182             if (!device.pushFile(hostFile, deviceDest, userId)) {
183                 throw new TargetSetupError(
184                         String.format(
185                                 "Failed to push local '%s' to remote '%s for user %d'",
186                                 hostFile.getAbsolutePath(), deviceDest, userId),
187                         device.getDeviceDescriptor(),
188                         DeviceErrorIdentifier.FAIL_PUSH_FILE);
189             }
190             mDeviceFilePushed = deviceDest;
191             if (!device.isPackageInstalled(ContentProviderHandler.PACKAGE_NAME)) {
192                 if (device instanceof NativeDevice) {
193                     var unused =
194                             ((NativeDevice) device).getContentProvider(device.getCurrentUser());
195                 }
196             }
197         }
198         // add host file to build
199         buildHelper.addDynamicConfigFile(mModuleName, hostFile);
200     }
201 
202     /** {@inheritDoc} */
203     @Override
tearDown(TestInformation testInfo, Throwable e)204     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
205         // Remove any file we have pushed to the device, host file will be moved to the result
206         // directory by ResultReporter upon invocation completion.
207         if (mDeviceFilePushed != null && !(e instanceof DeviceNotAvailableException) && mCleanup) {
208             testInfo.getDevice().deleteFile(mDeviceFilePushed);
209         }
210     }
211 
212     /**
213      * Return the the first element of test-suite-tag from configuration if it's not empty,
214      * otherwise, return the name from test-suite-info.properties.
215      */
216     @VisibleForTesting
getSuiteName()217     String getSuiteName() {
218         List<String> testSuiteTags = mModuleContext.getConfigurationDescriptor().getSuiteTags();
219         String suiteName = null;
220         if (!testSuiteTags.isEmpty()) {
221             if (testSuiteTags.size() >= 2) {
222                 CLog.i("More than 2 test-suite-tag are defined. test-suite-tag: " + testSuiteTags);
223             }
224             suiteName = testSuiteTags.get(0).toUpperCase();
225             CLog.i(
226                     "Replacing {suite-name} placeholder with %s from test suite tags in dynamic "
227                             + "config url.",
228                     suiteName);
229         } else {
230             suiteName = TestSuiteInfo.getInstance().getName();
231             CLog.i(
232                     "Replacing {suite-name} placeholder with %s from TestSuiteInfo in dynamic "
233                             + "config url.",
234                     suiteName);
235         }
236         return suiteName;
237     }
238 
239     @VisibleForTesting
getLocalConfigFile(CompatibilityBuildHelper buildHelper, ITestDevice device)240     final File getLocalConfigFile(CompatibilityBuildHelper buildHelper, ITestDevice device)
241             throws TargetSetupError {
242         File localConfigFile = null;
243         if (mExtractFromResource) {
244             String lookupName = (mResourceFileName != null) ? mResourceFileName : mModuleName;
245             InputStream dynamicFileRes = getClass().getResourceAsStream(
246                     String.format("/%s.dynamic", lookupName));
247             try {
248                 localConfigFile = FileUtil.createTempFile(lookupName, ".dynamic");
249                 FileUtil.writeToFile(dynamicFileRes, localConfigFile);
250             } catch (IOException e) {
251                 FileUtil.deleteFile(localConfigFile);
252                 throw new TargetSetupError(
253                         String.format("Fail to unpack '%s.dynamic' from resources", lookupName),
254                         e,
255                         device.getDeviceDescriptor(),
256                         InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
257             }
258             return localConfigFile;
259         }
260 
261         // If not from resources look at local path.
262         try {
263             String lookupName = (mDynamicConfigName != null) ? mDynamicConfigName : mModuleName;
264             localConfigFile = buildHelper.getTestFile(String.format("%s.dynamic", lookupName));
265         } catch (FileNotFoundException e) {
266             throw new TargetSetupError(
267                     "Cannot get local dynamic config file from test directory",
268                     e,
269                     device.getDeviceDescriptor(),
270                     InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
271         }
272         return localConfigFile;
273     }
274 
275     @VisibleForTesting
mergeConfigFiles( File localConfigFile, String apfeConfigInJson, String moduleName, ITestDevice device)276     File mergeConfigFiles(
277             File localConfigFile, String apfeConfigInJson, String moduleName, ITestDevice device)
278             throws TargetSetupError {
279         File hostFile = null;
280         try {
281             hostFile =
282                     DynamicConfigHandler.getMergedDynamicConfigFile(
283                             localConfigFile,
284                             apfeConfigInJson,
285                             moduleName,
286                             UrlReplacement.getUrlReplacementMap());
287             return hostFile;
288         } catch (IOException | XmlPullParserException | JSONException e) {
289             throw new TargetSetupError(
290                     "Cannot get merged dynamic config file", e, device.getDeviceDescriptor());
291         } finally {
292             if (mExtractFromResource) {
293                 FileUtil.deleteFile(localConfigFile);
294             }
295         }
296     }
297 
298     @VisibleForTesting
resolveUrl(String suiteName)299     String resolveUrl(String suiteName) throws TargetSetupError {
300         if (!mHasServerSideConfig) {
301             return null;
302         }
303         try {
304             String configUrl =
305                     UrlReplacement.getDynamicConfigServerUrl() == null
306                             ? mConfigUrl
307                             : UrlReplacement.getDynamicConfigServerUrl();
308             String requestUrl =
309                     configUrl
310                             .replace("{suite-name}", suiteName)
311                             .replace("{module}", mModuleName)
312                             .replace("{version}", mVersion)
313                             .replace("{api-key}", mApiKey);
314             java.net.URL request = new URL(requestUrl);
315             return StreamUtil.getStringFromStream(request.openStream());
316         } catch (IOException e) {
317             throw new TargetSetupError(
318                     String.format(
319                             "Trying to access android partner remote server over internet but"
320                                     + " failed: %s",
321                             e.getMessage()),
322                     e,
323                     null,
324                     false,
325                     InfraErrorIdentifier.ANDROID_PARTNER_SERVER_ERROR);
326         }
327     }
328 
createModuleName()329     public String createModuleName() {
330         // Device side utility already adds .dynamic extension
331         return String.format("%s", mModuleName);
332     }
333 }
334