1 /* 2 * Copyright (C) 2022 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 17 package com.android.tradefed.observatory; 18 19 import com.android.tradefed.build.IBuildInfo; 20 import com.android.tradefed.config.ArgsOptionParser; 21 import com.android.tradefed.config.ConfigurationException; 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.filter.GlobalTestFilter; 24 import com.android.tradefed.invoker.logger.InvocationMetricLogger; 25 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey; 26 import com.android.tradefed.invoker.tracing.CloseableTraceScope; 27 import com.android.tradefed.log.ITestLogger; 28 import com.android.tradefed.log.LogUtil.CLog; 29 import com.android.tradefed.result.FileInputStreamSource; 30 import com.android.tradefed.result.LogDataType; 31 import com.android.tradefed.result.error.InfraErrorIdentifier; 32 import com.android.tradefed.util.CommandResult; 33 import com.android.tradefed.util.CommandStatus; 34 import com.android.tradefed.util.FileUtil; 35 import com.android.tradefed.util.IRunUtil; 36 import com.android.tradefed.util.QuotationAwareTokenizer; 37 import com.android.tradefed.util.RunUtil; 38 import com.android.tradefed.util.StringEscapeUtils; 39 import com.android.tradefed.util.SystemUtil; 40 import com.android.tradefed.util.testmapping.TestMapping; 41 42 import com.google.common.annotations.VisibleForTesting; 43 import com.google.common.base.Joiner; 44 import com.google.common.base.Strings; 45 46 import org.json.JSONArray; 47 import org.json.JSONException; 48 import org.json.JSONObject; 49 50 import java.io.File; 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.Collections; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 60 /** 61 * A class for test launcher to call the TradeFed jar that packaged in the test suite to discover 62 * test modules. 63 * 64 * <p>TestDiscoveryInvoker will take {@link IConfiguration} and the test root directory from the 65 * launch control provider to make the launch control provider to invoke the workflow to use the 66 * config to query the packaged TradeFed jar file in the test suite root directory to retrieve test 67 * module names. 68 */ 69 public class TestDiscoveryInvoker { 70 71 private final IConfiguration mConfiguration; 72 private final String mDefaultConfigName; 73 private final File mRootDir; 74 private final IRunUtil mRunUtil = new RunUtil(); 75 private final boolean mHasConfigFallback; 76 private final boolean mUseCurrentTradefed; 77 private File mTestDir; 78 private File mTestMappingZip; 79 private IBuildInfo mBuildInfo; 80 private ITestLogger mLogger; 81 82 private static final TestDiscoveryUtil mTestDiscoveryUtil = new TestDiscoveryUtil(); 83 84 public static final String TRADEFED_OBSERVATORY_ENTRY_PATH = 85 TestDiscoveryExecutor.class.getName(); 86 public static final String TEST_DEPENDENCIES_LIST_KEY = "TestDependencies"; 87 public static final String TEST_MODULES_LIST_KEY = "TestModules"; 88 public static final String TEST_ZIP_REGEXES_LIST_KEY = "TestZipRegexes"; 89 90 public static final String TEST_DISCOVERY_COMMENT_KEY = "TestDiscoveryComment"; 91 public static final String PARTIAL_FALLBACK_KEY = "PartialFallback"; 92 public static final String NO_POSSIBLE_TEST_DISCOVERY_KEY = "NoPossibleTestDiscovery"; 93 public static final String TEST_MAPPING_ZIP_FILE = "TF_TEST_MAPPING_ZIP_FILE"; 94 public static final String ROOT_DIRECTORY_ENV_VARIABLE_KEY = 95 "ROOT_TEST_DISCOVERY_USE_TEST_DIRECTORY"; 96 97 public static final String OUTPUT_FILE = "DISCOVERY_OUTPUT_FILE"; 98 public static final String DISCOVERY_TRACE_FILE = "DISCOVERY_TRACE_FILE"; 99 public static final String BWYN_DISCOVER_TEST_ZIP = "BWYN_DISCOVER_TEST_ZIP"; 100 101 private static final long DISCOVERY_TIMEOUT_MS = 180000L; 102 103 @VisibleForTesting getRunUtil()104 IRunUtil getRunUtil() { 105 return mRunUtil; 106 } 107 108 @VisibleForTesting getJava()109 String getJava() { 110 return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath(); 111 } 112 113 @VisibleForTesting createOutputFile()114 File createOutputFile() throws IOException { 115 if (mTestDiscoveryUtil.hasOutputResultFile()) { 116 File presetOutputFile = new File(System.getenv(TestDiscoveryInvoker.OUTPUT_FILE)); 117 return presetOutputFile; 118 } 119 return FileUtil.createTempFile("discovery-output", ".txt"); 120 } 121 122 @VisibleForTesting createTraceFile()123 File createTraceFile() throws IOException { 124 return FileUtil.createTempFile("discovery-trace", ".txt"); 125 } 126 getTestDir()127 public File getTestDir() { 128 return mTestDir; 129 } 130 setTestDir(File testDir)131 public void setTestDir(File testDir) { 132 mTestDir = testDir; 133 } 134 setTestMappingZip(File testMappingZip)135 public void setTestMappingZip(File testMappingZip) { 136 mTestMappingZip = testMappingZip; 137 } 138 setBuildInfo(IBuildInfo buildInfo)139 public void setBuildInfo(IBuildInfo buildInfo) { 140 mBuildInfo = buildInfo; 141 } 142 setTestLogger(ITestLogger logger)143 public void setTestLogger(ITestLogger logger) { 144 mLogger = logger; 145 } 146 147 /** Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration} and root directory. */ TestDiscoveryInvoker(IConfiguration config, File rootDir)148 public TestDiscoveryInvoker(IConfiguration config, File rootDir) { 149 this(config, null, rootDir); 150 } 151 152 /** 153 * Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration}, test launcher's 154 * default config name and root directory. 155 */ TestDiscoveryInvoker(IConfiguration config, String defaultConfigName, File rootDir)156 public TestDiscoveryInvoker(IConfiguration config, String defaultConfigName, File rootDir) { 157 this(config, defaultConfigName, rootDir, false, false); 158 } 159 160 /** 161 * Creates an {@link TestDiscoveryInvoker} with a {@link IConfiguration}, test launcher's 162 * default config name, root directory and if fallback is required. 163 */ TestDiscoveryInvoker( IConfiguration config, String defaultConfigName, File rootDir, boolean hasConfigFallback, boolean useCurrentTradefed)164 public TestDiscoveryInvoker( 165 IConfiguration config, 166 String defaultConfigName, 167 File rootDir, 168 boolean hasConfigFallback, 169 boolean useCurrentTradefed) { 170 mConfiguration = config; 171 mDefaultConfigName = defaultConfigName; 172 mRootDir = rootDir; 173 mTestDir = null; 174 mHasConfigFallback = hasConfigFallback; 175 mUseCurrentTradefed = useCurrentTradefed; 176 } 177 178 /** 179 * Retrieve a map of xTS test dependency names - categorized by either test modules or other 180 * test dependencies. 181 * 182 * @return A map of test dependencies which grouped by TEST_MODULES_LIST_KEY and 183 * TEST_DEPENDENCIES_LIST_KEY. 184 * @throws IOException 185 * @throws JSONException 186 * @throws ConfigurationException 187 * @throws TestDiscoveryException 188 */ discoverTestDependencies()189 public Map<String, List<String>> discoverTestDependencies() 190 throws IOException, JSONException, ConfigurationException, TestDiscoveryException { 191 File outputFile = createOutputFile(); 192 File traceFile = createTraceFile(); 193 try (CloseableTraceScope ignored = new CloseableTraceScope("discoverTestDependencies")) { 194 Map<String, List<String>> dependencies = new HashMap<>(); 195 // Build the classpath base on test root directory which should contain all the jars 196 String classPath = buildXtsClasspath(mRootDir); 197 // Build command line args to query the tradefed.jar in the root directory 198 List<String> args = buildJavaCmdForXtsDiscovery(classPath); 199 String[] subprocessArgs = args.toArray(new String[args.size()]); 200 201 if (mHasConfigFallback) { 202 getRunUtil() 203 .setEnvVariable( 204 ROOT_DIRECTORY_ENV_VARIABLE_KEY, mRootDir.getAbsolutePath()); 205 } 206 getRunUtil().setEnvVariable(OUTPUT_FILE, outputFile.getAbsolutePath()); 207 getRunUtil().setEnvVariable(DISCOVERY_TRACE_FILE, traceFile.getAbsolutePath()); 208 CommandResult res = getRunUtil().runTimedCmd(DISCOVERY_TIMEOUT_MS, subprocessArgs); 209 String stdout = res.getStdout(); 210 CLog.i(String.format("Tradefed Observatory returned in stdout: %s", stdout)); 211 if (res.getExitCode() != 0 || !res.getStatus().equals(CommandStatus.SUCCESS)) { 212 DiscoveryExitCode exitCode = null; 213 if (res.getExitCode() != null) { 214 for (DiscoveryExitCode code : DiscoveryExitCode.values()) { 215 if (code.exitCode() == res.getExitCode()) { 216 exitCode = code; 217 } 218 } 219 } 220 if (DiscoveryExitCode.CONFIGURATION_EXCEPTION.equals(exitCode)) { 221 throw new ConfigurationException( 222 res.getStderr(), InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 223 } 224 throw new TestDiscoveryException( 225 String.format( 226 "Tradefed observatory error, unable to discover test module names." 227 + " command used: %s error: %s", 228 Joiner.on(" ").join(subprocessArgs), res.getStderr()), 229 null, 230 exitCode); 231 } 232 try (CloseableTraceScope discoResults = 233 new CloseableTraceScope("parse_discovery_results")) { 234 235 String result = FileUtil.readStringFromFile(outputFile); 236 CLog.i("output file content: %s", result); 237 238 // For backward compatibility 239 try { 240 new JSONObject(result); 241 } catch (JSONException e) { 242 CLog.w("Output file was incorrect. Try falling back stdout"); 243 result = stdout; 244 } 245 246 boolean noDiscovery = hasNoPossibleDiscovery(result); 247 if (noDiscovery) { 248 dependencies.put(NO_POSSIBLE_TEST_DISCOVERY_KEY, Arrays.asList("true")); 249 } 250 List<String> testModules = parseTestDiscoveryOutput(result, TEST_MODULES_LIST_KEY); 251 if (!noDiscovery) { 252 InvocationMetricLogger.addInvocationMetrics( 253 InvocationMetricKey.TEST_DISCOVERY_MODULE_COUNT, testModules.size()); 254 } 255 if (!testModules.isEmpty()) { 256 dependencies.put(TEST_MODULES_LIST_KEY, testModules); 257 } else { 258 // Only report no finding if discovery actually took effect 259 if (!noDiscovery) { 260 mConfiguration.getSkipManager().reportDiscoveryWithNoTests(); 261 } 262 } 263 264 List<String> testDependencies = 265 parseTestDiscoveryOutput(result, TEST_DEPENDENCIES_LIST_KEY); 266 if (!testDependencies.isEmpty()) { 267 dependencies.put(TEST_DEPENDENCIES_LIST_KEY, testDependencies); 268 } 269 270 String partialFallback = parsePartialFallback(result); 271 if (partialFallback != null) { 272 dependencies.put(PARTIAL_FALLBACK_KEY, Arrays.asList(partialFallback)); 273 } 274 if (!noDiscovery) { 275 mConfiguration 276 .getSkipManager() 277 .reportDiscoveryDependencies(testModules, testDependencies); 278 } 279 return dependencies; 280 } 281 } finally { 282 // Have output file set means other source is anticipating to read the output file. 283 // Therefore avoid deleting the output file if its set. (e.g. BWYN script) 284 if (!mTestDiscoveryUtil.hasOutputResultFile()) { 285 FileUtil.deleteFile(outputFile); 286 } 287 try (FileInputStreamSource source = new FileInputStreamSource(traceFile, true)) { 288 if (mLogger != null) { 289 mLogger.testLog("discovery-trace", LogDataType.PERFETTO, source); 290 } 291 } 292 } 293 } 294 295 /** 296 * Retrieve a map of test mapping test module names. 297 * 298 * @return A map of test module names which grouped by TEST_MODULES_LIST_KEY. 299 * @throws IOException 300 * @throws JSONException 301 * @throws ConfigurationException 302 * @throws TestDiscoveryException 303 */ discoverTestMappingDependencies()304 public Map<String, List<String>> discoverTestMappingDependencies() 305 throws IOException, JSONException, ConfigurationException, TestDiscoveryException { 306 File outputFile = createOutputFile(); 307 File traceFile = createTraceFile(); 308 try (CloseableTraceScope ignored = 309 new CloseableTraceScope("discoverTestMappingDependencies")) { 310 List<String> fullCommandLineArgs = 311 new ArrayList<String>( 312 Arrays.asList( 313 QuotationAwareTokenizer.tokenizeLine( 314 mConfiguration.getCommandLine()))); 315 // first arg is config name 316 fullCommandLineArgs.remove(0); 317 final ConfigurationTestMappingParserSettings mappingParserSettings = 318 new ConfigurationTestMappingParserSettings(); 319 ArgsOptionParser mappingOptionParser = new ArgsOptionParser(mappingParserSettings); 320 // Parse to collect all values of --cts-params as well config name 321 mappingOptionParser.parseBestEffort(fullCommandLineArgs, true); 322 323 Map<String, List<String>> dependencies = new HashMap<>(); 324 // Build the classpath base on the working directory 325 String classPath = buildTestMappingClasspath(mRootDir); 326 // Build command line args to query the tradefed.jar in the working directory 327 List<String> args = buildJavaCmdForTestMappingDiscovery(classPath); 328 String[] subprocessArgs = args.toArray(new String[args.size()]); 329 330 // Pass the test mapping zip path to subprocess by environment variable 331 if (mTestMappingZip != null) { 332 getRunUtil() 333 .setEnvVariable(TEST_MAPPING_ZIP_FILE, mTestMappingZip.getAbsolutePath()); 334 } 335 if (mBuildInfo != null) { 336 if (mBuildInfo.getFile(TestMapping.TEST_MAPPINGS_ZIP) != null) { 337 getRunUtil() 338 .setEnvVariable( 339 TEST_MAPPING_ZIP_FILE, 340 mBuildInfo 341 .getFile(TestMapping.TEST_MAPPINGS_ZIP) 342 .getAbsolutePath()); 343 getRunUtil() 344 .setEnvVariable( 345 TestMapping.TEST_MAPPINGS_ZIP, 346 mBuildInfo 347 .getFile(TestMapping.TEST_MAPPINGS_ZIP) 348 .getAbsolutePath()); 349 } 350 for (String allowedList : mappingParserSettings.mAllowedTestLists) { 351 if (mBuildInfo.getFile(allowedList) != null) { 352 getRunUtil() 353 .setEnvVariable( 354 allowedList, 355 mBuildInfo.getFile(allowedList).getAbsolutePath()); 356 } 357 } 358 } 359 if (!Strings.isNullOrEmpty(mappingParserSettings.mRunTestSuite)) { 360 throw new TestDiscoveryException( 361 String.format( 362 "Test discovery for test suite is not supported yet. Test suite:" 363 + " %s", 364 mappingParserSettings.mRunTestSuite), 365 null); 366 } 367 368 if (mHasConfigFallback) { 369 getRunUtil() 370 .setEnvVariable( 371 ROOT_DIRECTORY_ENV_VARIABLE_KEY, mRootDir.getAbsolutePath()); 372 } 373 getRunUtil().setEnvVariable(DISCOVERY_TRACE_FILE, traceFile.getAbsolutePath()); 374 getRunUtil().setEnvVariable(OUTPUT_FILE, outputFile.getAbsolutePath()); 375 CommandResult res = getRunUtil().runTimedCmd(DISCOVERY_TIMEOUT_MS, subprocessArgs); 376 String stdout = res.getStdout(); 377 CLog.i(String.format("Tradefed Observatory returned in stdout:\n %s", stdout)); 378 if (res.getExitCode() != 0 || !res.getStatus().equals(CommandStatus.SUCCESS)) { 379 throw new TestDiscoveryException( 380 String.format( 381 "Tradefed observatory error, unable to discover test module names." 382 + " command used: %s error: %s", 383 Joiner.on(" ").join(subprocessArgs), res.getStderr()), 384 null); 385 } 386 try (CloseableTraceScope discoResults = 387 new CloseableTraceScope("parse_discovery_results")) { 388 String result = FileUtil.readStringFromFile(outputFile); 389 390 boolean noDiscovery = hasNoPossibleDiscovery(result); 391 if (noDiscovery) { 392 dependencies.put(NO_POSSIBLE_TEST_DISCOVERY_KEY, Arrays.asList("true")); 393 } 394 List<String> testModules = parseTestDiscoveryOutput(result, TEST_MODULES_LIST_KEY); 395 if (!noDiscovery) { 396 InvocationMetricLogger.addInvocationMetrics( 397 InvocationMetricKey.TEST_DISCOVERY_MODULE_COUNT, testModules.size()); 398 } 399 if (!testModules.isEmpty()) { 400 dependencies.put(TEST_MODULES_LIST_KEY, testModules); 401 } else { 402 if (!noDiscovery) { 403 mConfiguration.getSkipManager().reportDiscoveryWithNoTests(); 404 } 405 } 406 String partialFallback = parsePartialFallback(result); 407 if (partialFallback != null) { 408 dependencies.put(PARTIAL_FALLBACK_KEY, Arrays.asList(partialFallback)); 409 } 410 if (!noDiscovery) { 411 if (!noDiscovery) { 412 mConfiguration 413 .getSkipManager() 414 .reportDiscoveryDependencies(testModules, new ArrayList<String>()); 415 } 416 } 417 return dependencies; 418 } 419 } finally { 420 // Have output file set means other source is anticipating to read the output file. 421 // Therefore avoid deleting the output file if its set. (e.g. BWYN script) 422 if (!mTestDiscoveryUtil.hasOutputResultFile()) { 423 FileUtil.deleteFile(outputFile); 424 } 425 try (FileInputStreamSource source = new FileInputStreamSource(traceFile, true)) { 426 if (mLogger != null) { 427 mLogger.testLog("discovery-trace", LogDataType.PERFETTO, source); 428 } 429 } 430 } 431 } 432 433 /** 434 * Build java cmd for invoking a subprocess to discover test mapping test module names. 435 * 436 * @return A list of java command args. 437 */ buildJavaCmdForTestMappingDiscovery(String classpath)438 private List<String> buildJavaCmdForTestMappingDiscovery(String classpath) { 439 List<String> fullCommandLineArgs = 440 new ArrayList<String>( 441 Arrays.asList( 442 QuotationAwareTokenizer.tokenizeLine( 443 mConfiguration.getCommandLine()))); 444 445 List<String> args = new ArrayList<>(); 446 447 args.add(getJava()); 448 449 args.add("-cp"); 450 args.add(classpath); 451 452 args.add(TRADEFED_OBSERVATORY_ENTRY_PATH); 453 454 // Delete invocation data from args which test discovery don't need 455 int i = 0; 456 while (i < fullCommandLineArgs.size()) { 457 if (fullCommandLineArgs.get(i).equals("--invocation-data")) { 458 i = i + 2; 459 } else { 460 args.add(fullCommandLineArgs.get(i)); 461 i = i + 1; 462 } 463 } 464 return args; 465 } 466 467 /** 468 * Build java cmd for invoking a subprocess to discover XTS test module names. 469 * 470 * @return A list of java command args. 471 * @throws ConfigurationException 472 */ buildJavaCmdForXtsDiscovery(String classpath)473 private List<String> buildJavaCmdForXtsDiscovery(String classpath) 474 throws ConfigurationException, TestDiscoveryException { 475 List<String> fullCommandLineArgs = 476 new ArrayList<String>( 477 Arrays.asList( 478 QuotationAwareTokenizer.tokenizeLine( 479 mConfiguration.getCommandLine()))); 480 // first arg is config name 481 fullCommandLineArgs.remove(0); 482 483 final ConfigurationCtsParserSettings ctsParserSettings = 484 new ConfigurationCtsParserSettings(); 485 ArgsOptionParser ctsOptionParser = new ArgsOptionParser(ctsParserSettings); 486 487 // Parse to collect all values of --cts-params as well config name 488 ctsOptionParser.parseBestEffort(fullCommandLineArgs, true); 489 490 List<String> ctsParams = ctsParserSettings.mCtsParams; 491 for (String globalFilter : ctsParserSettings.mStrictIncludeFilters) { 492 ctsParams.add("--" + GlobalTestFilter.STRICT_INCLUDE_FILTER_OPTION); 493 ctsParams.add(globalFilter); 494 } 495 String configName = ctsParserSettings.mConfigName; 496 497 if (configName == null) { 498 if (mDefaultConfigName == null) { 499 throw new TestDiscoveryException( 500 String.format( 501 "Failed to extract config-name from parent test command options," 502 + " unable to build args to invoke tradefed observatory." 503 + " Parent test command options is: %s", 504 fullCommandLineArgs), 505 null, 506 DiscoveryExitCode.ERROR); 507 } else { 508 CLog.i( 509 String.format( 510 "No config name provided in the command args, use default config" 511 + " name %s", 512 mDefaultConfigName)); 513 configName = mDefaultConfigName; 514 } 515 } 516 List<String> args = new ArrayList<>(); 517 args.add(getJava()); 518 519 args.add("-cp"); 520 args.add(classpath); 521 522 // Cts V2 requires CTS_ROOT to be set or VTS_ROOT for vts run 523 args.add( 524 String.format( 525 "-D%s=%s", ctsParserSettings.mRootdirVar, mRootDir.getAbsolutePath())); 526 527 args.add(TRADEFED_OBSERVATORY_ENTRY_PATH); 528 args.add(configName); 529 530 // Tokenize args to be passed to CtsTest/XtsTest 531 args.addAll(StringEscapeUtils.paramsToArgs(ctsParams)); 532 533 return args; 534 } 535 536 /** 537 * Build the classpath string based on jars in the sandbox's working directory. 538 * 539 * @return A string of classpaths. 540 * @throws IOException 541 */ buildTestMappingClasspath(File workingDir)542 private String buildTestMappingClasspath(File workingDir) throws IOException { 543 try (CloseableTraceScope ignored = new CloseableTraceScope("build_classpath")) { 544 List<String> classpathList = new ArrayList<>(); 545 546 if (!workingDir.exists()) { 547 throw new FileNotFoundException("Couldn't find the build directory"); 548 } 549 550 if (workingDir.listFiles().length == 0) { 551 throw new FileNotFoundException( 552 String.format( 553 "Could not find any files under %s", workingDir.getAbsolutePath())); 554 } 555 for (File toolsFile : workingDir.listFiles()) { 556 if (toolsFile.getName().endsWith(".jar")) { 557 classpathList.add(toolsFile.getAbsolutePath()); 558 } 559 } 560 Collections.sort(classpathList); 561 if (mUseCurrentTradefed) { 562 classpathList.add(getCurrentClassPath()); 563 } 564 565 return Joiner.on(":").join(classpathList); 566 } 567 } 568 getCurrentClassPath()569 private String getCurrentClassPath() { 570 return System.getProperty("java.class.path"); 571 } 572 573 /** 574 * Build the classpath string based on jars in the XTS test root directory's tools folder. 575 * 576 * @return A string of classpaths. 577 * @throws IOException 578 */ buildXtsClasspath(File ctsRoot)579 private String buildXtsClasspath(File ctsRoot) throws IOException { 580 List<File> classpathList = new ArrayList<>(); 581 582 if (!ctsRoot.exists()) { 583 throw new FileNotFoundException("Couldn't find the build directory: " + ctsRoot); 584 } 585 586 // Safe to assume single dir from extracted zip 587 if (ctsRoot.list().length != 1) { 588 throw new RuntimeException( 589 "List of sub directory does not contain only one item " 590 + "current list is:" 591 + Arrays.toString(ctsRoot.list())); 592 } 593 String mainDirName = ctsRoot.list()[0]; 594 // Jar files from the downloaded cts/xts 595 File jarCtsPath = new File(new File(ctsRoot, mainDirName), "tools"); 596 if (jarCtsPath.listFiles().length == 0) { 597 throw new FileNotFoundException( 598 String.format( 599 "Could not find any files under %s", jarCtsPath.getAbsolutePath())); 600 } 601 for (File toolsFile : jarCtsPath.listFiles()) { 602 if (toolsFile.getName().endsWith(".jar")) { 603 classpathList.add(toolsFile); 604 } 605 } 606 Collections.sort(classpathList); 607 608 return Joiner.on(":").join(classpathList); 609 } 610 611 /** 612 * Parse test module names from the tradefed observatory's output JSON string. 613 * 614 * @param discoveryOutput JSON string from test discovery 615 * @param dependencyListKey test dependency type 616 * @return A list of test module names. 617 * @throws JSONException 618 */ parseTestDiscoveryOutput(String discoveryOutput, String dependencyListKey)619 private List<String> parseTestDiscoveryOutput(String discoveryOutput, String dependencyListKey) 620 throws JSONException { 621 JSONObject jsonObject = new JSONObject(discoveryOutput); 622 List<String> testModules = new ArrayList<>(); 623 if (jsonObject.has(dependencyListKey)) { 624 JSONArray jsonArray = jsonObject.getJSONArray(dependencyListKey); 625 for (int i = 0; i < jsonArray.length(); i++) { 626 testModules.add(jsonArray.getString(i)); 627 } 628 } 629 return testModules; 630 } 631 parsePartialFallback(String discoveryOutput)632 private String parsePartialFallback(String discoveryOutput) throws JSONException { 633 JSONObject jsonObject = new JSONObject(discoveryOutput); 634 if (jsonObject.has(PARTIAL_FALLBACK_KEY)) { 635 return jsonObject.getString(PARTIAL_FALLBACK_KEY); 636 } 637 return null; 638 } 639 hasNoPossibleDiscovery(String discoveryOutput)640 private boolean hasNoPossibleDiscovery(String discoveryOutput) throws JSONException { 641 JSONObject jsonObject = new JSONObject(discoveryOutput); 642 if (jsonObject.has(NO_POSSIBLE_TEST_DISCOVERY_KEY)) { 643 return true; 644 } 645 return false; 646 } 647 } 648