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