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