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