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