1 /* 2 * Copyright (C) 2019 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.tradefed.cluster; 17 18 import com.android.tradefed.config.ArgsOptionParser; 19 import com.android.tradefed.config.Configuration; 20 import com.android.tradefed.config.ConfigurationException; 21 import com.android.tradefed.config.DeviceConfigurationHolder; 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.IDeviceConfiguration; 24 import com.android.tradefed.log.SimpleFileLogger; 25 import com.android.tradefed.result.ITestInvocationListener; 26 import com.android.tradefed.util.MultiMap; 27 import com.android.tradefed.util.StringUtil; 28 import com.android.tradefed.util.UniqueMultiMap; 29 30 import com.google.common.annotations.VisibleForTesting; 31 32 import org.json.JSONException; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.io.PrintWriter; 37 import java.lang.reflect.InvocationTargetException; 38 import java.nio.file.Paths; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.TreeMap; 44 45 /** A class to build a configuration file for a cluster command. */ 46 public class ClusterCommandConfigBuilder { 47 48 private static final String TEST_TAG = "cluster_command_launcher"; 49 50 private ClusterCommand mCommand; 51 private TestEnvironment mTestEnvironment; 52 private List<TestResource> mTestResources; 53 private TestContext mTestContext; 54 private File mWorkDir; 55 56 /** 57 * Set a {@link ClusterCommand} object. 58 * 59 * @param command a {@link ClusterCommand} object. 60 * @return {@link ClusterCommandConfigBuilder} for chaining. 61 */ setClusterCommand(ClusterCommand command)62 public ClusterCommandConfigBuilder setClusterCommand(ClusterCommand command) { 63 mCommand = command; 64 return this; 65 } 66 67 /** 68 * Set a {@link TestEnvironment} object. 69 * 70 * @param testEnvironment a {@link TestEnvironment} object. 71 * @return {@link ClusterCommandConfigBuilder} for chaining. 72 */ setTestEnvironment(TestEnvironment testEnvironment)73 public ClusterCommandConfigBuilder setTestEnvironment(TestEnvironment testEnvironment) { 74 mTestEnvironment = testEnvironment; 75 return this; 76 } 77 78 /** 79 * Set a list of {@link TestResource} object. 80 * 81 * @param testResources a list of {@link TestResource} objects. 82 * @return {@link ClusterCommandConfigBuilder} for chaining. 83 */ setTestResources(List<TestResource> testResources)84 public ClusterCommandConfigBuilder setTestResources(List<TestResource> testResources) { 85 mTestResources = testResources; 86 return this; 87 } 88 89 /** 90 * Set a {@link TestContext} object. 91 * 92 * @param testContext a {@link TestContext} object. 93 * @return {@link ClusterCommandConfigBuilder} for chaining. 94 */ setTestContext(TestContext testContext)95 public ClusterCommandConfigBuilder setTestContext(TestContext testContext) { 96 mTestContext = testContext; 97 return this; 98 } 99 100 /** 101 * Set a work directory for a command. 102 * 103 * @param workDir a work directory. 104 * @return {@link ClusterCommandConfigBuilder} for chaining. 105 */ setWorkDir(File workDir)106 public ClusterCommandConfigBuilder setWorkDir(File workDir) { 107 mWorkDir = workDir; 108 return this; 109 } 110 111 /** Get a {@link IConfiguration} object type name for {@link TradefedConfigObject.Type}. */ getConfigObjectTypeName(TradefedConfigObject.Type type)112 private String getConfigObjectTypeName(TradefedConfigObject.Type type) { 113 switch (type) { 114 case TARGET_PREPARER: 115 return Configuration.TARGET_PREPARER_TYPE_NAME; 116 case RESULT_REPORTER: 117 return Configuration.RESULT_REPORTER_TYPE_NAME; 118 default: 119 throw new UnsupportedOperationException(String.format("%s is not supported", type)); 120 } 121 } 122 123 /** Create a {@link IConfiguration} object for a {@link TradefedConfigObject}. */ createConfigObject( TradefedConfigObject configObjDef, Map<String, String> envVars)124 private Object createConfigObject( 125 TradefedConfigObject configObjDef, Map<String, String> envVars) 126 throws ConfigurationException { 127 Object configObj = null; 128 try { 129 configObj = 130 Class.forName(configObjDef.getClassName()) 131 .getDeclaredConstructor() 132 .newInstance(); 133 } catch (InstantiationException 134 | IllegalAccessException 135 | ClassNotFoundException 136 | InvocationTargetException 137 | NoSuchMethodException e) { 138 throw new ConfigurationException( 139 String.format( 140 "Failed to add a config object '%s'", configObjDef.getClassName()), 141 e); 142 } 143 MultiMap<String, String> optionValues = configObjDef.getOptionValues(); 144 List<String> optionArgs = new ArrayList<>(); 145 for (String name : optionValues.keySet()) { 146 List<String> values = optionValues.get(name); 147 for (String value : values) { 148 optionArgs.add(String.format("--%s", name)); 149 if (value != null) { 150 // value can be null for valueless options. 151 optionArgs.add(StringUtil.expand(value, envVars)); 152 } 153 } 154 } 155 ArgsOptionParser parser = new ArgsOptionParser(configObj); 156 parser.parse(optionArgs); 157 return configObj; 158 } 159 160 @VisibleForTesting initConfiguration()161 IConfiguration initConfiguration() { 162 return new Configuration("Cluster Command " + mCommand.getCommandId(), ""); 163 } 164 165 @VisibleForTesting getSystemEnvMap()166 Map<String, String> getSystemEnvMap() { 167 return System.getenv(); 168 } 169 170 /** 171 * Builds a configuration file. 172 * 173 * @return a {@link File} object for a generated configuration file. 174 */ build()175 public File build() throws ConfigurationException, IOException, JSONException { 176 assert mCommand != null; 177 assert mTestEnvironment != null; 178 assert mTestResources != null; 179 assert mWorkDir != null; 180 181 IConfiguration config = initConfiguration(); 182 config.getCommandOptions().setTestTag(TEST_TAG); 183 List<IDeviceConfiguration> deviceConfigs = new ArrayList<>(); 184 int index = 0; 185 assert 0 < mCommand.getTargetDeviceSerials().size(); 186 187 // Split config defs into device/non-device ones. 188 List<TradefedConfigObject> deviceConfigObjDefs = new ArrayList<>(); 189 List<TradefedConfigObject> nonDeviceConfigObjDefs = new ArrayList<>(); 190 for (TradefedConfigObject configObjDef : mTestEnvironment.getTradefedConfigObjects()) { 191 if (TradefedConfigObject.Type.TARGET_PREPARER.equals(configObjDef.getType())) { 192 deviceConfigObjDefs.add(configObjDef); 193 } else { 194 nonDeviceConfigObjDefs.add(configObjDef); 195 } 196 } 197 198 Map<String, String> envVars = new TreeMap<>(); 199 Map<String, String> systemEnvMap = getSystemEnvMap(); 200 envVars.putAll(systemEnvMap); 201 202 envVars.put("TF_WORK_DIR", mWorkDir.getAbsolutePath()); 203 envVars.put("TF_ATTEMPT_ID", mCommand.getAttemptId()); 204 envVars.putAll(mTestEnvironment.getEnvVars()); 205 envVars.putAll(mTestContext.getEnvVars()); 206 207 for (String serial : mCommand.getTargetDeviceSerials()) { 208 serial = ClusterHostUtil.getLocalDeviceSerial(serial); 209 IDeviceConfiguration device = 210 new DeviceConfigurationHolder(String.format("TF_DEVICE_%d", index++)); 211 device.getDeviceRequirements().setSerial(serial); 212 for (TradefedConfigObject configObjDef : deviceConfigObjDefs) { 213 device.addSpecificConfig(createConfigObject(configObjDef, envVars)); 214 } 215 device.addSpecificConfig(new ClusterBuildProvider()); 216 deviceConfigs.add(device); 217 } 218 config.setDeviceConfigList(deviceConfigs); 219 // Perform target preparation in parallel with an unlimited timeout 220 if (mTestEnvironment.useParallelSetup()) { 221 config.injectOptionValue("parallel-setup", "true"); 222 config.injectOptionValue("parallel-setup-timeout", "0"); 223 } 224 225 config.setTest(new ClusterCommandLauncher()); 226 config.setLogSaver(new ClusterLogSaver()); 227 // TODO(b/135636270): return log path to TFC instead of relying on a specific filename 228 config.setLogOutput(new SimpleFileLogger()); 229 config.injectOptionValue( 230 "simple-file:path", 231 Paths.get(mWorkDir.getAbsolutePath(), "logs", "host_log.txt").toString()); 232 config.setTestInvocationListeners(Collections.<ITestInvocationListener>emptyList()); 233 for (TradefedConfigObject configObjDef : nonDeviceConfigObjDefs) { 234 String typeName = getConfigObjectTypeName(configObjDef.getType()); 235 @SuppressWarnings("unchecked") 236 List<Object> configObjs = (List<Object>) config.getConfigurationObjectList(typeName); 237 configObjs.add(createConfigObject(configObjDef, envVars)); 238 config.setConfigurationObjectList(typeName, configObjs); 239 } 240 241 config.injectOptionValue("cluster:request-id", mCommand.getRequestId()); 242 config.injectOptionValue("cluster:command-id", mCommand.getCommandId()); 243 config.injectOptionValue("cluster:attempt-id", mCommand.getAttemptId()); 244 // FIXME: Make this configurable. 245 config.injectOptionValue("enable-root", "false"); 246 247 String commandLine = mTestContext.getCommandLine(); 248 if (commandLine == null || commandLine.isEmpty()) { 249 commandLine = mCommand.getCommandLine(); 250 } 251 config.injectOptionValue("cluster:command-line", commandLine); 252 config.injectOptionValue("cluster:original-command-line", mCommand.getCommandLine()); 253 config.injectOptionValue("cluster:root-dir", mWorkDir.getAbsolutePath()); 254 255 for (final Map.Entry<String, String> entry : envVars.entrySet()) { 256 config.injectOptionValue("cluster:env-var", entry.getKey(), entry.getValue()); 257 } 258 for (final String script : mTestEnvironment.getSetupScripts()) { 259 config.injectOptionValue("cluster:setup-script", script); 260 } 261 if (mTestEnvironment.useSubprocessReporting()) { 262 config.injectOptionValue("cluster:use-subprocess-reporting", "true"); 263 } 264 config.getCommandOptions().setInvocationTimeout(mTestEnvironment.getInvocationTimeout()); 265 config.injectOptionValue( 266 "cluster:output-idle-timeout", 267 String.valueOf(mTestEnvironment.getOutputIdleTimeout())); 268 for (String option : mTestEnvironment.getJvmOptions()) { 269 config.injectOptionValue("cluster:jvm-option", option); 270 } 271 for (final Map.Entry<String, String> entry : 272 mTestEnvironment.getJavaProperties().entrySet()) { 273 config.injectOptionValue("cluster:java-property", entry.getKey(), entry.getValue()); 274 } 275 if (mTestEnvironment.getOutputFileUploadUrl() != null) { 276 String baseUrl = mTestEnvironment.getOutputFileUploadUrl(); 277 if (!baseUrl.endsWith("/")) { 278 baseUrl += "/"; 279 } 280 final String url = 281 String.format( 282 "%s%s/%s/", baseUrl, mCommand.getCommandId(), mCommand.getAttemptId()); 283 config.injectOptionValue( 284 "cluster:output-file-upload-url", StringUtil.expand(url, envVars)); 285 } 286 for (final String pattern : mTestEnvironment.getOutputFilePatterns()) { 287 config.injectOptionValue("cluster:output-file-pattern", pattern); 288 } 289 if (mTestEnvironment.getContextFilePattern() != null) { 290 config.injectOptionValue( 291 "cluster:context-file-pattern", mTestEnvironment.getContextFilePattern()); 292 } 293 for (String file : mTestEnvironment.getExtraContextFiles()) { 294 config.injectOptionValue("cluster:extra-context-file", file); 295 } 296 if (mTestEnvironment.getRetryCommandLine() != null) { 297 config.injectOptionValue( 298 "cluster:retry-command-line", mTestEnvironment.getRetryCommandLine()); 299 } 300 if (mTestEnvironment.getLogLevel() != null) { 301 config.injectOptionValue("log-level", mTestEnvironment.getLogLevel()); 302 } 303 for (String excludedFile : mTestEnvironment.getExcludedFilesInJavaClasspath()) { 304 config.injectOptionValue("cluster:exclude-file-in-java-classpath", excludedFile); 305 } 306 307 List<TestResource> testResources = new ArrayList<>(); 308 testResources.addAll(mTestResources); 309 testResources.addAll(mTestContext.getTestResources()); 310 for (final TestResource resource : testResources) { 311 config.injectOptionValue( 312 "cluster:test-resource", 313 StringUtil.expand(resource.toJson().toString(), envVars)); 314 } 315 316 // Inject any extra options into the configuration 317 UniqueMultiMap<String, String> extraOptions = mCommand.getExtraOptions(); 318 for (String key : extraOptions.keySet()) { 319 for (String value : extraOptions.get(key)) { 320 config.injectOptionValue(key, StringUtil.expand(value, envVars)); 321 } 322 } 323 324 File f = new File(mWorkDir, "command.xml"); 325 PrintWriter writer = new PrintWriter(f); 326 config.dumpXml(writer); 327 writer.close(); 328 return f; 329 } 330 } 331