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