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