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