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.uicd.tests; 17 18 import com.android.tradefed.build.IBuildInfo; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.config.Option.Importance; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.result.FileInputStreamSource; 25 import com.android.tradefed.result.ITestInvocationListener; 26 import com.android.tradefed.result.InputStreamSource; 27 import com.android.tradefed.result.LogDataType; 28 import com.android.tradefed.result.TestDescription; 29 import com.android.tradefed.testtype.IMultiDeviceTest; 30 import com.android.tradefed.testtype.IRemoteTest; 31 import com.android.tradefed.util.CommandResult; 32 import com.android.tradefed.util.FileUtil; 33 import com.android.tradefed.util.MultiMap; 34 import com.android.tradefed.util.RunUtil; 35 import java.io.File; 36 import java.io.IOException; 37 import java.nio.file.Paths; 38 import java.util.ArrayList; 39 import java.util.Collection; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.UUID; 44 import org.json.JSONArray; 45 import org.json.JSONException; 46 import org.json.JSONObject; 47 48 /** 49 * The class enables user to run their pre-recorded UICD tests on tradefed. Go to 50 * https://github.com/google/android-uiconductor/releases/tag/v0.1.1 to download the uicd_cli.tar.gz 51 * and extract the jar and apks required for the tests. Please look at the sample xmls in 52 * res/config/uicd to configure your tests. 53 */ 54 public class UiConductorTest implements IMultiDeviceTest, IRemoteTest { 55 56 @Option( 57 name = "uicd-cli-jar", 58 description = "The cli jar that runs the user provided tests in commandline", 59 importance = Importance.IF_UNSET 60 ) 61 private File cliJar; 62 63 @Option( 64 name = "commandline-action-executable", 65 description = 66 "the filesystem path of the binaries that are ran through command line actions on UICD. Can be repeated.", 67 importance = Importance.IF_UNSET 68 ) 69 private Collection<File> binaries = new ArrayList<File>(); 70 71 @Option( 72 name = "global-variables", 73 description = "Global variable (uicd_key1=value1,uicd_key2=value2)", 74 importance = Importance.ALWAYS 75 ) 76 private MultiMap<String, String> globalVariables = new MultiMap<>(); 77 78 @Option( 79 name = "play-mode", 80 description = "Play Mode (SINGLE|MULTIDEVICE|PLAYALL).", 81 importance = Importance.ALWAYS 82 ) 83 private String playMode = "SINGLE"; 84 85 @Option(name = "test-name", description = "Name of the test.", importance = Importance.ALWAYS) 86 private String testName = "Your test results are here"; 87 88 // Same key can have multiple test files because global-variables can be referenced using the 89 // that particular key and shared across different tests. 90 // Refer res/config/uicd/uiconductor-globalvariable-sample.xml for more information. 91 @Option( 92 name = "uicd-test", 93 description = 94 "the filesystem path of the json test files or directory of multiple json test files that needs to be run on devices. Can be repeated.", 95 importance = Importance.IF_UNSET 96 ) 97 private MultiMap<String, File> uicdTest = new MultiMap<>(); 98 99 @Option( 100 name = "test-timeout", 101 description = "Time out for each test.", 102 importance = Importance.IF_UNSET 103 ) 104 private int testTimeout = 1800000; 105 106 private static final String BINARY_RELATIVE_PATH = "binary"; 107 108 private static final String OUTPUT_RELATIVE_PATH = "output"; 109 110 private static final String TESTS_RELATIVE_PATH = "tests"; 111 112 private static final String RESULTS_RELATIVE_PATH = "result"; 113 114 private static final String OPTION_SYMBOL = "-"; 115 private static final String INPUT_OPTION_SHORT_NAME = "i"; 116 private static final String OUTPUT_OPTION_SHORT_NAME = "o"; 117 private static final String DEVICES_OPTION_SHORT_NAME = "d"; 118 private static final String MODE_OPTION_SHORT_NAME = "m"; 119 private static final String GLOBAL_VARIABLE_OPTION_SHORT_NAME = "g"; 120 121 private static final String CHILDRENRESULT_ATTRIBUTE = "childrenResult"; 122 private static final String PLAYSTATUS_ATTRIBUTE = "playStatus"; 123 private static final String VALIDATIONDETAILS_ATTRIBUTE = "validationDetails"; 124 125 private static final String EXECUTABLE = "u+x"; 126 127 private static String baseFilePath = System.getenv("HOME") + "/tmp/uicd-on-tf"; 128 129 Map<ITestDevice, IBuildInfo> deviceInfos; 130 131 @Override setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos)132 public void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) { 133 this.deviceInfos = deviceInfos; 134 } 135 136 @Override run(ITestInvocationListener listener)137 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 138 CLog.i("Starting the UIConductor tests:\n"); 139 String runId = UUID.randomUUID().toString(); 140 baseFilePath = Paths.get(baseFilePath, runId).toString(); 141 String jarFileDir = Paths.get(baseFilePath, BINARY_RELATIVE_PATH).toString(); 142 String testFilesDir = Paths.get(baseFilePath, TESTS_RELATIVE_PATH).toString(); 143 String binaryFilesDir = Paths.get(baseFilePath).toString(); 144 File jarFile; 145 MultiMap<String, File> copiedTestFileMap = new MultiMap<>(); 146 if (cliJar == null || !cliJar.exists()) { 147 CLog.e("Unable to fetch provided binary.\n"); 148 return; 149 } 150 try { 151 jarFile = copyFile(cliJar.getAbsolutePath(), jarFileDir); 152 FileUtil.chmod(jarFile, EXECUTABLE); 153 154 for (Map.Entry<String, File> testFileOrDirEntry : uicdTest.entries()) { 155 copiedTestFileMap.putAll( 156 copyFile( 157 testFileOrDirEntry.getKey(), 158 testFileOrDirEntry.getValue().getAbsolutePath(), 159 testFilesDir)); 160 } 161 162 for (File binaryFile : binaries) { 163 File binary = copyFile(binaryFile.getAbsolutePath(), binaryFilesDir); 164 FileUtil.chmod(binary, EXECUTABLE); 165 } 166 } catch (IOException ex) { 167 throw new DeviceNotAvailableException(ex.getMessage()); 168 } 169 170 RunUtil rUtil = new RunUtil(); 171 rUtil.setWorkingDir(new File(baseFilePath)); 172 long runStartTime = System.currentTimeMillis(); 173 listener.testRunStarted(testName, copiedTestFileMap.values().size()); 174 for (Map.Entry<String, File> testFileEntry : copiedTestFileMap.entries()) { 175 runTest( 176 listener, 177 rUtil, 178 jarFile, 179 testFileEntry.getKey(), 180 testFileEntry.getValue().getName()); 181 } 182 183 listener.testRunEnded( 184 System.currentTimeMillis() - runStartTime, new HashMap<String, String>()); 185 FileUtil.recursiveDelete(new File(baseFilePath)); 186 CLog.i("Finishing the ui conductor tests\n"); 187 } 188 runTest( ITestInvocationListener listener, RunUtil rUtil, File jarFile, String key, String testFileName)189 public void runTest( 190 ITestInvocationListener listener, 191 RunUtil rUtil, 192 File jarFile, 193 String key, 194 String testFileName) { 195 TestDescription testDesc = 196 new TestDescription(this.getClass().getSimpleName(), testFileName); 197 listener.testStarted(testDesc, System.currentTimeMillis()); 198 199 String testId = UUID.randomUUID().toString(); 200 CommandResult cmndRes = 201 rUtil.runTimedCmd(testTimeout, getCommand(jarFile, testFileName, testId, key)); 202 logInfo(testId, "STD", cmndRes.getStdout()); 203 logInfo(testId, "ERR", cmndRes.getStderr()); 204 205 File resultsFile = 206 new File( 207 Paths.get( 208 baseFilePath, 209 OUTPUT_RELATIVE_PATH, 210 testId, 211 RESULTS_RELATIVE_PATH, 212 "action_execution_result") 213 .toString()); 214 215 if (resultsFile.exists()) { 216 try { 217 String content = FileUtil.readStringFromFile(resultsFile); 218 JSONObject result = new JSONObject(content); 219 List<String> errors = new ArrayList<>(); 220 errors = parseResult(errors, result); 221 if (!errors.isEmpty()) { 222 listener.testFailed(testDesc, errors.get(0)); 223 CLog.i("Test %s failed due to following errors: \n", testDesc.getTestName()); 224 for (String error : errors) { 225 CLog.i(error + "\n"); 226 } 227 } 228 } catch (IOException | JSONException e) { 229 CLog.e(e); 230 } 231 String testResultFileName = testFileName + "_action_execution_result"; 232 try (InputStreamSource iSSource = new FileInputStreamSource(resultsFile)) { 233 listener.testLog(testResultFileName, LogDataType.TEXT, iSSource); 234 } 235 } 236 listener.testEnded(testDesc, System.currentTimeMillis(), new HashMap<String, String>()); 237 } 238 logInfo(String testId, String cmdOutputType, String content)239 private void logInfo(String testId, String cmdOutputType, String content) { 240 CLog.i( 241 "===========================" 242 + cmdOutputType 243 + " logs for " 244 + testId 245 + " starts===========================\n"); 246 CLog.i(content); 247 CLog.i( 248 "===========================" 249 + cmdOutputType 250 + " logs for " 251 + testId 252 + " ends===========================\n"); 253 } 254 parseResult(List<String> errors, JSONObject result)255 private List<String> parseResult(List<String> errors, JSONObject result) throws JSONException { 256 257 if (result != null) { 258 if (result.has(CHILDRENRESULT_ATTRIBUTE)) { 259 JSONArray childResults = result.getJSONArray(CHILDRENRESULT_ATTRIBUTE); 260 for (int i = 0; i < childResults.length(); i++) { 261 errors = parseResult(errors, childResults.getJSONObject(i)); 262 } 263 } 264 265 if (result.has(PLAYSTATUS_ATTRIBUTE) 266 && result.getString(PLAYSTATUS_ATTRIBUTE).equalsIgnoreCase("FAIL")) { 267 if (result.has(VALIDATIONDETAILS_ATTRIBUTE)) { 268 errors.add(result.getString(VALIDATIONDETAILS_ATTRIBUTE)); 269 } 270 } 271 } 272 return errors; 273 } 274 copyFile(String srcFilePath, String destDirPath)275 private File copyFile(String srcFilePath, String destDirPath) throws IOException { 276 File srcFile = new File(srcFilePath); 277 File destDir = new File(destDirPath); 278 if (srcFile.isDirectory()) { 279 for (File file : srcFile.listFiles()) { 280 copyFile(file.getAbsolutePath(), Paths.get(destDirPath, file.getName()).toString()); 281 } 282 } 283 if (!destDir.isDirectory() && !destDir.mkdirs()) { 284 throw new IOException( 285 String.format("Could not create directory %s", destDir.getAbsolutePath())); 286 } 287 File destFile = new File(Paths.get(destDir.toString(), srcFile.getName()).toString()); 288 FileUtil.copyFile(srcFile, destFile); 289 return destFile; 290 } 291 292 // copy file to destDirPath while maintaining a map of key that refers to that src file copyFile(String key, String srcFilePath, String destDirPath)293 private MultiMap<String, File> copyFile(String key, String srcFilePath, String destDirPath) 294 throws IOException { 295 MultiMap<String, File> copiedTestFileMap = new MultiMap<>(); 296 File srcFile = new File(srcFilePath); 297 File destDir = new File(destDirPath); 298 if (srcFile.isDirectory()) { 299 for (File file : srcFile.listFiles()) { 300 copiedTestFileMap.putAll( 301 copyFile( 302 key, 303 file.getAbsolutePath(), 304 Paths.get(destDirPath, file.getName()).toString())); 305 } 306 } 307 if (!destDir.isDirectory() && !destDir.mkdirs()) { 308 throw new IOException( 309 String.format("Could not create directory %s", destDir.getAbsolutePath())); 310 } 311 if (srcFile.isFile()) { 312 File destFile = new File(Paths.get(destDir.toString(), srcFile.getName()).toString()); 313 FileUtil.copyFile(srcFile, destFile); 314 copiedTestFileMap.put(key, destFile); 315 } 316 return copiedTestFileMap; 317 } 318 getTestFilesArgsForUicdBin(String testFilesDir, String filename)319 private String getTestFilesArgsForUicdBin(String testFilesDir, String filename) { 320 return (!testFilesDir.isEmpty() && !filename.isEmpty()) 321 ? Paths.get(testFilesDir, filename).toString() 322 : ""; 323 } 324 getOutFilesArgsForUicdBin(String outFilesDir)325 private String getOutFilesArgsForUicdBin(String outFilesDir) { 326 return !outFilesDir.isEmpty() ? outFilesDir : ""; 327 } 328 getPlaymodeArgForUicdBin()329 private String getPlaymodeArgForUicdBin() { 330 return !playMode.isEmpty() ? playMode : ""; 331 } 332 getDevIdsArgsForUicdBin()333 private String getDevIdsArgsForUicdBin() { 334 List<String> devIds = new ArrayList<>(); 335 for (ITestDevice device : deviceInfos.keySet()) { 336 devIds.add(device.getSerialNumber()); 337 } 338 return String.join(",", devIds); 339 } 340 getCommand(File jarFile, String testFileName, String testId, String key)341 private String[] getCommand(File jarFile, String testFileName, String testId, String key) { 342 List<String> command = new ArrayList<>(); 343 command.add("java"); 344 command.add("-jar"); 345 command.add(jarFile.getAbsolutePath()); 346 if (!getTestFilesArgsForUicdBin(TESTS_RELATIVE_PATH, testFileName).isEmpty()) { 347 command.add(OPTION_SYMBOL + INPUT_OPTION_SHORT_NAME); 348 command.add(getTestFilesArgsForUicdBin(TESTS_RELATIVE_PATH, testFileName)); 349 } 350 if (!getOutFilesArgsForUicdBin(OUTPUT_RELATIVE_PATH + "/" + testId).isEmpty()) { 351 command.add(OPTION_SYMBOL + OUTPUT_OPTION_SHORT_NAME); 352 command.add(getOutFilesArgsForUicdBin(OUTPUT_RELATIVE_PATH + "/" + testId)); 353 } 354 if (!getPlaymodeArgForUicdBin().isEmpty()) { 355 command.add(OPTION_SYMBOL + MODE_OPTION_SHORT_NAME); 356 command.add(getPlaymodeArgForUicdBin()); 357 } 358 if (!getDevIdsArgsForUicdBin().isEmpty()) { 359 command.add(OPTION_SYMBOL + DEVICES_OPTION_SHORT_NAME); 360 command.add(getDevIdsArgsForUicdBin()); 361 } 362 if (globalVariables.containsKey(key)) { 363 command.add(OPTION_SYMBOL + GLOBAL_VARIABLE_OPTION_SHORT_NAME); 364 command.add(String.join(",", globalVariables.get(key))); 365 } 366 return command.toArray(new String[] {}); 367 } 368 } 369