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