• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.util.testmapping;
17 
18 import com.android.tradefed.build.IBuildInfo;
19 import com.android.tradefed.config.ConfigurationException;
20 import com.android.tradefed.error.HarnessRuntimeException;
21 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.observatory.TestDiscoveryInvoker;
24 import com.android.tradefed.result.error.InfraErrorIdentifier;
25 import com.android.tradefed.util.FileUtil;
26 import com.android.tradefed.util.ZipUtil2;
27 
28 import com.google.common.annotations.VisibleForTesting;
29 import com.google.common.base.Strings;
30 
31 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
32 import org.apache.commons.compress.archivers.zip.ZipFile;
33 import org.json.JSONArray;
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 import org.json.JSONTokener;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.nio.charset.StandardCharsets;
41 import java.nio.file.FileVisitOption;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.nio.file.Paths;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Collections;
48 import java.util.Enumeration;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.Iterator;
52 import java.util.LinkedHashMap;
53 import java.util.LinkedHashSet;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Set;
57 import java.util.regex.Matcher;
58 import java.util.regex.Pattern;
59 import java.util.stream.Stream;
60 
61 /** A class for loading a TEST_MAPPING file. */
62 public class TestMapping {
63 
64     // Key for test sources information stored in metadata of ConfigurationDescription.
65     public static final String TEST_SOURCES = "Test Sources";
66     // Pattern used to identify mainline tests without parameterized modules configured.
67     public static final Pattern MAINLINE_REGEX = Pattern.compile("(\\S+)\\[(\\S+)\\]");
68 
69     private static final String PRESUBMIT = "presubmit";
70     private static final String IMPORTS = "imports";
71     private static final String KEY_IMPORT_PATH = "path";
72     private static final String KEY_HOST = "host";
73     private static final String KEY_KEYWORDS = "keywords";
74     private static final String KEY_FILE_PATTERNS = "file_patterns";
75     private static final String KEY_NAME = "name";
76     private static final String KEY_OPTIONS = "options";
77     private static final String TEST_MAPPING = "TEST_MAPPING";
78     public static final String TEST_MAPPINGS_ZIP = "test_mappings.zip";
79     // A file containing module names that are disabled in presubmit test runs.
80     private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
81     // Pattern used to identify comments start with "//" or "#" in TEST_MAPPING.
82     private static final Pattern COMMENTS_REGEX = Pattern.compile(
83             "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
84     private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
85 
86     private List<String> mTestMappingRelativePaths = new ArrayList<>();
87 
88     private boolean mIgnoreTestMappingImports = true;
89 
90     /** Constructor to initialize an empty {@link TestMapping} object. */
TestMapping()91     public TestMapping() {
92         this(new ArrayList<>(), true);
93     }
94 
95     /**
96      * Constructor to create a {@link TestMapping} object.
97      *
98      * @param testMappingRelativePaths The {@link List<String>} to the TEST_MAPPING file paths.
99      * @param ignoreTestMappingImports The {@link boolean} to ignore imports.
100      */
TestMapping(List<String> testMappingRelativePaths, boolean ignoreTestMappingImports)101     public TestMapping(List<String> testMappingRelativePaths, boolean ignoreTestMappingImports) {
102         mTestMappingRelativePaths = testMappingRelativePaths;
103         mIgnoreTestMappingImports = ignoreTestMappingImports;
104     }
105 
106     /**
107      * Helper to get the {@link Map} test collection from a path to TEST_MAPPING file.
108      *
109      * @param path The {@link Path} to a TEST_MAPPING file.
110      * @param testMappingsDir The {@link Path} to the folder of all TEST_MAPPING files for a build.
111      * @param matchedPatternPaths The {@link Set<String>} to file paths matched patterns.
112      * @return A {@link Map} of test collection.
113      */
114     @VisibleForTesting
getTestCollection( Path path, Path testMappingsDir, Set<String> matchedPatternPaths)115     Map<String, Set<TestInfo>> getTestCollection(
116             Path path, Path testMappingsDir, Set<String> matchedPatternPaths) {
117         Map<String, Set<TestInfo>> testCollection = new LinkedHashMap<>();
118         String relativePath = testMappingsDir.relativize(path.getParent()).toString();
119         String errorMessage = null;
120         if (Files.notExists(path)) {
121             CLog.d("TEST_MAPPING path not found: %s.", path);
122             return testCollection;
123         }
124         try {
125             String content = removeComments(
126                     String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)));
127             if (Strings.isNullOrEmpty(content)) {
128                 return testCollection;
129             }
130             JSONTokener tokener = new JSONTokener(content);
131             JSONObject root = new JSONObject(tokener);
132             Iterator<String> testGroups = (Iterator<String>) root.keys();
133 
134             Set<Path> filePaths = new HashSet<>();
135             if (!mIgnoreTestMappingImports) {
136                 listTestMappingFiles(path.getParent(), testMappingsDir, filePaths);
137             }
138 
139             while (testGroups.hasNext()) {
140                 String group = testGroups.next();
141                 if (group.equals(IMPORTS)) {
142                     continue;
143                 }
144                 Set<TestInfo> testsForGroup = new LinkedHashSet<>();
145                 testCollection.put(group, testsForGroup);
146                 JSONArray arr = root.getJSONArray(group);
147                 for (int i = 0; i < arr.length(); i++) {
148                     JSONObject testObject = arr.getJSONObject(i);
149                     boolean hostOnly = testObject.has(KEY_HOST) && testObject.getBoolean(KEY_HOST);
150                     Set<String> keywords = new HashSet<>();
151                     if (testObject.has(KEY_KEYWORDS)) {
152                         JSONArray keywordArray = testObject.getJSONArray(KEY_KEYWORDS);
153                         for (int j = 0; j < keywordArray.length(); j++) {
154                             keywords.add(keywordArray.getString(j));
155                         }
156                     }
157                     Set<String> filePatterns = new HashSet<>();
158                     if (testObject.has(KEY_FILE_PATTERNS)) {
159                         JSONArray filePatternArray = testObject.getJSONArray(KEY_FILE_PATTERNS);
160                         for (int k = 0; k < filePatternArray.length(); k++) {
161                             filePatterns.add(filePatternArray.getString(k));
162                         }
163                     }
164                     if (!isMatchedFilePatterns(relativePath, matchedPatternPaths, filePatterns)) {
165                         continue;
166                     }
167                     TestInfo test =
168                             new TestInfo(
169                                     testObject.getString(KEY_NAME),
170                                     relativePath,
171                                     hostOnly,
172                                     keywords);
173                     if (testObject.has(KEY_OPTIONS)) {
174                         JSONArray optionObjects = testObject.getJSONArray(KEY_OPTIONS);
175                         for (int j = 0; j < optionObjects.length(); j++) {
176                             JSONObject optionObject = optionObjects.getJSONObject(j);
177                             for (int k = 0; k < optionObject.names().length(); k++) {
178                                 String name = optionObject.names().getString(k);
179                                 String value = optionObject.getString(name);
180                                 TestOption option = new TestOption(name, value);
181                                 test.addOption(option);
182                             }
183                         }
184                     }
185                     for (Path filePath : filePaths) {
186                         Path importPath = testMappingsDir.relativize(filePath).getParent();
187                         if (!test.getSources().contains(importPath.toString())) {
188                             test.addImportPaths(Collections.singleton(importPath.toString()));
189                         }
190                     }
191                     testsForGroup.add(test);
192                 }
193             }
194 
195             if (!mIgnoreTestMappingImports) {
196                 // No longer need to include import paths, filePaths includes all related paths.
197                 for (Path filePath : filePaths) {
198                     Map<String, Set<TestInfo>> filePathImportedTestCollection =
199                             new TestMapping(mTestMappingRelativePaths, true)
200                                     .getTestCollection(
201                                             filePath, testMappingsDir, matchedPatternPaths);
202                     for (String group : filePathImportedTestCollection.keySet()) {
203                         // Add all imported TestInfo to testCollection.
204                         if (filePathImportedTestCollection.get(group) != null) {
205                             if (testCollection.get(group) == null) {
206                                 testCollection.put(
207                                         group, filePathImportedTestCollection.get(group));
208                             } else {
209                                 testCollection
210                                         .get(group)
211                                         .addAll(filePathImportedTestCollection.get(group));
212                             }
213                         }
214                     }
215                 }
216             }
217         } catch (IOException e) {
218             errorMessage = String.format("TEST_MAPPING file does not exist: %s.", path.toString());
219             CLog.e(errorMessage);
220         } catch (JSONException e) {
221             errorMessage =
222                     String.format(
223                             "Error parsing TEST_MAPPING file: %s. Error: %s", path.toString(), e);
224         }
225 
226         if (errorMessage != null) {
227             CLog.e(errorMessage);
228             throw new HarnessRuntimeException(
229                     errorMessage, InfraErrorIdentifier.TEST_MAPPING_FILE_FORMAT_ISSUE);
230         }
231         return testCollection;
232     }
233 
234     /**
235      * Helper to check whether the given matched-pattern-paths matches the file patterns.
236      *
237      * @param testMappingDir A {@link String} to TEST_MAPPING directory path.
238      * @param matchedPatternPaths A {@link Set<String>} to file paths matched patterns.
239      * @param filePatterns A {@link Set<String>} to filePatterns from a TEST_MAPPING file.
240      * @return A {@link Boolean} of matched result.
241      */
isMatchedFilePatterns( String testMappingDir, Set<String> matchedPatternPaths, Set<String> filePatterns)242     private boolean isMatchedFilePatterns(
243             String testMappingDir, Set<String> matchedPatternPaths, Set<String> filePatterns) {
244         Set<String> matchedPatternPathsInSource = new HashSet<>();
245         for (String matchedPatternPath : matchedPatternPaths) {
246             if (matchedPatternPath.matches(
247                     String.join(File.separator, new String[] {testMappingDir, ".*"}))) {
248                 Path relativePath =
249                         Paths.get(testMappingDir).relativize(Paths.get(matchedPatternPath));
250                 matchedPatternPathsInSource.add(relativePath.toString());
251             }
252         }
253         // For POSTSUBMIT runs, Test Mapping should run the full tests, so return true when
254         // mTestMappingRelativePaths is empty.
255         if (mTestMappingRelativePaths.isEmpty() || filePatterns.isEmpty()) {
256             return true;
257         }
258         for (String matchedPatternPathInSource : matchedPatternPathsInSource) {
259             Path matchedPatternFilePath = Paths.get(matchedPatternPathInSource);
260             if (matchedPatternFilePath.getFileName().toString().equals(TEST_MAPPING)) {
261                 return true;
262             }
263             for (String filePattern : filePatterns) {
264                 if (matchedPatternPathInSource.matches(filePattern)) {
265                     return true;
266                 }
267             }
268         }
269         return false;
270     }
271 
272     /**
273      * Helper to remove comments in a TEST_MAPPING file to valid format. Only "//" and "#" are
274      * regarded as comments.
275      *
276      * @param jsonContent A {@link String} of json which content is from a TEST_MAPPING file.
277      * @return A {@link String} of valid json without comments.
278      */
279     @VisibleForTesting
removeComments(String jsonContent)280     String removeComments(String jsonContent) {
281         StringBuffer out = new StringBuffer();
282         Matcher matcher = COMMENTS_REGEX.matcher(jsonContent);
283         while (matcher.find()) {
284             if (COMMENTS.contains(matcher.group(1))) {
285                 matcher.appendReplacement(out, "");
286             }
287         }
288         matcher.appendTail(out);
289         return out.toString();
290     }
291 
292     /**
293      * Helper to list all test mapping files, look for all parent directories and related import
294      * paths.
295      *
296      * @param testMappingDir The {@link Path} to a TEST_MAPPING file parent directory.
297      * @param testMappingsRootDir The {@link Path} to the folder of all TEST_MAPPING files for a
298      *     build.
299      * @param filePaths A {@link Set<Path>} to store all TEST_MAPPING paths.
300      */
listTestMappingFiles( Path testMappingDir, Path testMappingsRootDir, Set<Path> filePaths)301     public void listTestMappingFiles(
302             Path testMappingDir, Path testMappingsRootDir, Set<Path> filePaths) {
303         String errorMessage = null;
304 
305         if (!testMappingDir.toAbsolutePath().startsWith(testMappingsRootDir.toAbsolutePath())) {
306             CLog.d(
307                     "SKIPPED: Path %s is not under test mapping directory %s.",
308                     testMappingDir, testMappingsRootDir);
309             return;
310         }
311 
312         if (Files.notExists(testMappingDir)) {
313             CLog.d("TEST_MAPPING path not found: %s.", testMappingDir);
314             return;
315         }
316 
317         try {
318             Path testMappingPath = testMappingDir.resolve(TEST_MAPPING);
319             filePaths.add(testMappingPath);
320             String content =
321                     removeComments(
322                             String.join(
323                                     "\n",
324                                     Files.readAllLines(testMappingPath, StandardCharsets.UTF_8)));
325             if (Strings.isNullOrEmpty(content)) {
326                 return;
327             }
328             JSONTokener tokener = new JSONTokener(content);
329             JSONObject root = new JSONObject(tokener);
330             if (root.has(IMPORTS) && !mIgnoreTestMappingImports) {
331                 JSONArray arr = root.getJSONArray(IMPORTS);
332                 for (int i = 0; i < arr.length(); i++) {
333                     JSONObject testObject = arr.getJSONObject(i);
334                     Path importPath = Paths.get(testObject.getString(KEY_IMPORT_PATH));
335                     Path normImportPath =
336                             Paths.get(testMappingsRootDir.toString(), importPath.toString());
337 
338                     Path importPathTestMappingPath = normImportPath.resolve(TEST_MAPPING);
339                     if (!filePaths.contains(importPathTestMappingPath)) {
340                         if (Files.exists(importPathTestMappingPath)) {
341                             filePaths.add(importPathTestMappingPath);
342                         }
343                         listTestMappingFiles(importPath, testMappingsRootDir, filePaths);
344                     }
345                 }
346             }
347 
348             while (testMappingDir
349                     .toAbsolutePath()
350                     .startsWith(testMappingsRootDir.toAbsolutePath())) {
351                 if (testMappingDir.toAbsolutePath().equals(testMappingsRootDir.toAbsolutePath())) {
352                     break;
353                 }
354                 Path upperDirectory = testMappingDir.getParent();
355                 Path upperDirectoryTestMappingPath = upperDirectory.resolve(TEST_MAPPING);
356                 if (Files.exists(upperDirectoryTestMappingPath)
357                         && !filePaths.contains(upperDirectoryTestMappingPath)) {
358                     filePaths.add(upperDirectoryTestMappingPath);
359                     listTestMappingFiles(upperDirectory, testMappingsRootDir, filePaths);
360                 }
361                 testMappingDir = upperDirectory;
362             }
363 
364         } catch (IOException e) {
365             errorMessage =
366                     String.format(
367                             "Error reading TEST_MAPPING file: %s.", testMappingDir.toString());
368         } catch (JSONException e) {
369             errorMessage =
370                     String.format(
371                             "Error parsing TEST_MAPPING file: %s. Error: %s",
372                             testMappingDir.toString(), e);
373         }
374 
375         if (errorMessage != null) {
376             CLog.e(errorMessage);
377             throw new RuntimeException(errorMessage);
378         }
379     }
380 
381     /**
382      * Helper to get all tests set from the given test collection, group name, disabled tests, host
383      * test only, and keywords.
384      *
385      * @param testCollection A {@link Map} of the test collection.
386      * @param testGroup A {@link String} of the test group.
387      * @param disabledTests A set of {@link String} for the name of the disabled tests.
388      * @param hostOnly true if only tests running on host and don't require device should be
389      *     returned. false to return tests that require device to run.
390      * @param keywords A set of {@link String} to be matched when filtering tests to run in a Test
391      *     Mapping suite.
392      * @param ignoreKeywords A set of {@link String} of keywords to be ignored.
393      * @return A {@code Set<TestInfo>} of the test infos.
394      */
395     @VisibleForTesting
getTests( Map<String, Set<TestInfo>> testCollection, String testGroup, Set<String> disabledTests, boolean hostOnly, Set<String> keywords, Set<String> ignoreKeywords)396     Set<TestInfo> getTests(
397             Map<String, Set<TestInfo>> testCollection,
398             String testGroup,
399             Set<String> disabledTests,
400             boolean hostOnly,
401             Set<String> keywords,
402             Set<String> ignoreKeywords) {
403         Set<TestInfo> tests = new LinkedHashSet<TestInfo>();
404         for (TestInfo test : testCollection.getOrDefault(testGroup, new HashSet<>())) {
405             if (disabledTests != null && disabledTests.contains(test.getName())) {
406                 continue;
407             }
408             if (test.getHostOnly() != hostOnly) {
409                 continue;
410             }
411             Set<String> testKeywords = test.getKeywords(ignoreKeywords);
412             // Skip the test if no keyword is specified but the test requires certain keywords.
413             if ((keywords == null || keywords.isEmpty()) && !testKeywords.isEmpty()) {
414                 continue;
415             }
416             // Skip the test if any of the required keywords is not specified by the test.
417             if (keywords != null) {
418                 boolean allKeywordsFound = true;
419                 for (String keyword : keywords) {
420                     if (!testKeywords.contains(keyword)) {
421                         allKeywordsFound = false;
422                         break;
423                     }
424                 }
425                 // The test should be skipped if any keyword is missing in the test configuration.
426                 if (!allKeywordsFound) {
427                     continue;
428                 }
429             }
430             tests.add(test);
431         }
432 
433         return tests;
434     }
435 
436     /**
437      * Helper to find all tests in all TEST_MAPPING files based on an artifact in the device build.
438      *
439      * @param buildInfo the {@link IBuildInfo} describing the build.
440      * @param testGroup a {@link String} of the test group.
441      * @param hostOnly true if only tests running on host and don't require device should be
442      *     returned. false to return tests that require device to run.
443      * @param keywords A set of {@link String} to be matched when filtering tests to run in a Test
444      *     Mapping suite.
445      * @param ignoreKeywords A set of {@link String} of keywords to be ignored.
446      * @return A {@code Set<TestInfo>} of tests set in the build artifact, test_mappings.zip.
447      */
getTests( IBuildInfo buildInfo, String testGroup, boolean hostOnly, Set<String> keywords, Set<String> ignoreKeywords)448     public Set<TestInfo> getTests(
449             IBuildInfo buildInfo,
450             String testGroup,
451             boolean hostOnly,
452             Set<String> keywords,
453             Set<String> ignoreKeywords) {
454         return getTests(
455                 buildInfo,
456                 testGroup,
457                 hostOnly,
458                 keywords,
459                 ignoreKeywords,
460                 new ArrayList<>(),
461                 new HashSet<>());
462     }
463 
464     /**
465      * Helper to find all tests in all TEST_MAPPING files based on the given artifact. This is
466      * needed when a suite run requires to run all tests in TEST_MAPPING files for a given group,
467      * e.g., presubmit.
468      *
469      * @param buildInfo the {@link IBuildInfo} describing the build.
470      * @param testGroup a {@link String} of the test group.
471      * @param hostOnly true if only tests running on host and don't require device should be
472      *     returned. false to return tests that require device to run.
473      * @param keywords A set of {@link String} to be matched when filtering tests to run in a Test
474      *     Mapping suite.
475      * @param ignoreKeywords A set of {@link String} of keywords to be ignored.
476      * @param extraZipNames A set of {@link String} for the name of additional test_mappings.zip
477      *     that will be merged.
478      * @param matchedPatternPaths The {@link Set<String>} to file paths matched patterns.
479      * @return A {@code Set<TestInfo>} of tests set in the build artifact, test_mappings.zip.
480      */
getTests( IBuildInfo buildInfo, String testGroup, boolean hostOnly, Set<String> keywords, Set<String> ignoreKeywords, List<String> extraZipNames, Set<String> matchedPatternPaths)481     public Set<TestInfo> getTests(
482             IBuildInfo buildInfo,
483             String testGroup,
484             boolean hostOnly,
485             Set<String> keywords,
486             Set<String> ignoreKeywords,
487             List<String> extraZipNames,
488             Set<String> matchedPatternPaths) {
489         Set<TestInfo> tests = Collections.synchronizedSet(new LinkedHashSet<TestInfo>());
490         File zipFile;
491         if (buildInfo == null) {
492             zipFile = lookupTestMappingZip(TEST_MAPPINGS_ZIP);
493         } else {
494             zipFile = buildInfo.getFile(TEST_MAPPINGS_ZIP);
495         }
496         File testMappingsDir = null;
497         boolean delete = false;
498         // Handle already extracted entry
499         if (zipFile.isDirectory()) {
500             testMappingsDir = zipFile;
501         } else {
502             testMappingsDir = extractTestMappingsZip(zipFile);
503             delete = true;
504         }
505         Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
506         CLog.d("Relative test mapping paths: %s", mTestMappingRelativePaths);
507         try (Stream<Path> stream =
508                 mTestMappingRelativePaths.isEmpty()
509                         ? Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS)
510                         : getAllTestMappingPaths(testMappingsRootPath).stream()) {
511             mergeTestMappingZips(buildInfo, extraZipNames, zipFile, testMappingsDir);
512             Set<String> disabledTests = getDisabledTests(testMappingsRootPath, testGroup);
513             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
514                     .forEach(
515                             path ->
516                                     tests.addAll(
517                                             getTests(
518                                                     getTestCollection(
519                                                             path,
520                                                             testMappingsRootPath,
521                                                             matchedPatternPaths),
522                                                     testGroup,
523                                                     disabledTests,
524                                                     hostOnly,
525                                                     keywords,
526                                                     ignoreKeywords)));
527 
528         } catch (IOException e) {
529             throw new RuntimeException(
530                     String.format(
531                             "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
532                             e.getMessage(), testMappingsDir.getAbsolutePath()), e);
533         } catch (NoTestRuntimeException e) {
534             if (System.getenv("ALLOW_EMPTY_TEST_MAPPING") == null) {
535                 throw e;
536             } else {
537                 CLog.d("Allowing empty test info lists.");
538             }
539         } finally {
540             if (delete) {
541                 FileUtil.recursiveDelete(testMappingsDir);
542             }
543         }
544         return tests;
545     }
546 
547     /**
548      * Helper to get all TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
549      *
550      * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
551      * @return A {@code Set<Path>} of all the TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
552      */
553     @VisibleForTesting
getAllTestMappingPaths(Path testMappingsRootPath)554     Set<Path> getAllTestMappingPaths(Path testMappingsRootPath) {
555         Set<Path> allTestMappingPaths = new LinkedHashSet<>();
556         for (String path : mTestMappingRelativePaths) {
557             boolean hasAdded = false;
558             Path testMappingPath = testMappingsRootPath.resolve(path);
559             // Recursively find the TEST_MAPPING file until reaching to testMappingsRootPath.
560             while (!testMappingPath.equals(testMappingsRootPath)) {
561                 if (testMappingPath.resolve(TEST_MAPPING).toFile().exists()) {
562                     hasAdded = true;
563                     CLog.d("Adding TEST_MAPPING path: %s", testMappingPath);
564                     allTestMappingPaths.add(testMappingPath.resolve(TEST_MAPPING));
565                 }
566                 testMappingPath = testMappingPath.getParent();
567             }
568             if (!hasAdded) {
569                 CLog.w("Couldn't find TEST_MAPPING files from %s", path);
570             }
571         }
572         if (allTestMappingPaths.isEmpty()) {
573             throw new NoTestRuntimeException(
574                     String.format(
575                             "Couldn't find TEST_MAPPING files from %s", mTestMappingRelativePaths));
576         }
577         CLog.d("All resolved TEST_MAPPING paths: %s", allTestMappingPaths);
578         return allTestMappingPaths;
579     }
580 
581     public class NoTestRuntimeException extends RuntimeException {
582 
NoTestRuntimeException(String message)583         public NoTestRuntimeException(String message) {
584             super(message);
585         }
586     }
587 
588     /**
589      * Helper to find all tests in the TEST_MAPPING files from a given directory.
590      *
591      * @param testMappingsDir the {@link File} the directory containing all Test Mapping files.
592      * @return A {@code Map<String, Set<TestInfo>>} of tests in the given directory and its child
593      *     directories.
594      */
getAllTests(File testMappingsDir)595     public Map<String, Set<TestInfo>> getAllTests(File testMappingsDir) {
596         Map<String, Set<TestInfo>> allTests = new HashMap<String, Set<TestInfo>>();
597         Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
598         try (Stream<Path> stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS)) {
599             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
600                     .forEach(
601                             path ->
602                                     getAllTests(allTests, path, testMappingsRootPath));
603 
604         } catch (IOException e) {
605             throw new RuntimeException(
606                     String.format(
607                             "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
608                             e.getMessage(), testMappingsDir.getAbsolutePath()),
609                     e);
610         }
611         return allTests;
612     }
613 
614     /**
615      * Helper to find all tests in the TEST_MAPPING files from a given directory.
616      *
617      * @param allTests the {@code HashMap<String, Set<TestInfo>>} containing the tests of each test
618      *     group.
619      * @param path the {@link Path} to a TEST_MAPPING file.
620      * @param testMappingsRootPath the {@link Path} to a test mappings zip path.
621      */
getAllTests( Map<String, Set<TestInfo>> allTests, Path path, Path testMappingsRootPath)622     private void getAllTests(
623             Map<String, Set<TestInfo>> allTests, Path path, Path testMappingsRootPath) {
624         Map<String, Set<TestInfo>> testCollection =
625                 getTestCollection(path, testMappingsRootPath, new HashSet<>());
626         for (String group : testCollection.keySet()) {
627             allTests.computeIfAbsent(group, k -> new HashSet<>()).addAll(testCollection.get(group));
628         }
629     }
630 
631     /**
632      * Extract a zip file and return the directory that contains the content of unzipped files.
633      *
634      * @param testMappingsZip A {@link File} of the test mappings zip to extract.
635      * @return a {@link File} pointing to the temp directory for test mappings zip.
636      */
extractTestMappingsZip(File testMappingsZip)637     public static File extractTestMappingsZip(File testMappingsZip) {
638         File testMappingsDir = null;
639         try (CloseableTraceScope ignored = new CloseableTraceScope("extractTestMappingsZip")) {
640             testMappingsDir = ZipUtil2.extractZipToTemp(testMappingsZip, TEST_MAPPINGS_ZIP);
641         } catch (IOException e) {
642             throw new RuntimeException(
643                     String.format(
644                             "IO exception (%s) when extracting test mappings zip (%s)",
645                             e.getMessage(), testMappingsZip.getAbsolutePath()), e);
646         }
647         return testMappingsDir;
648     }
649 
650     /**
651      * Get disabled tests from test mapping artifact.
652      *
653      * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
654      * @param testGroup a {@link String} of the test group.
655      * @return a {@link Set<String>} containing all the disabled presubmit tests. No test is
656      *     returned if the testGroup is not PRESUBMIT.
657      */
658     @VisibleForTesting
getDisabledTests(Path testMappingsRootPath, String testGroup)659     Set<String> getDisabledTests(Path testMappingsRootPath, String testGroup) {
660         Set<String> disabledTests = new HashSet<>();
661         File disabledPresubmitTestsFile =
662                 new File(testMappingsRootPath.toString(), DISABLED_PRESUBMIT_TESTS_FILE);
663         if (!(testGroup.equals(PRESUBMIT) && disabledPresubmitTestsFile.exists())) {
664             return disabledTests;
665         }
666         try {
667             disabledTests.addAll(
668                     Arrays.asList(
669                             FileUtil.readStringFromFile(disabledPresubmitTestsFile)
670                                     .split("\\r?\\n")));
671         } catch (IOException e) {
672             throw new RuntimeException(
673                     String.format(
674                             "IO exception (%s) when reading disabled tests from file (%s)",
675                             e.getMessage(), disabledPresubmitTestsFile.getAbsolutePath()), e);
676         }
677         return disabledTests;
678     }
679 
680     /**
681      * Helper to get the matcher for parameterized mainline tests.
682      *
683      * @param {@code Set<TestInfo>} of tests set in the build artifact, test_mappings.zip.
684      * @return A {@link Matcher} for parameterized mainline tests.
685      */
getMainlineTestModuleName(TestInfo info)686     public static Matcher getMainlineTestModuleName(TestInfo info) throws ConfigurationException {
687         Matcher matcher = MAINLINE_REGEX.matcher(info.getName());
688         if (matcher.find()) {
689             return matcher;
690         }
691         throw new ConfigurationException(
692                 String.format(
693                         "Unmatched \"[]\" for \"%s\" configured in the %s. "
694                                 + "Parameter must contain square brackets.",
695                         info.getName(), info.getSources()));
696     }
697 
698     /**
699      * Merge additional test mapping zips into the given directory.
700      *
701      * @param buildInfo the {@link IBuildInfo} describing the build.
702      * @param extraZips A {@link List<String>} of additional zip file paths.
703      * @param baseFile A {@link File} of base test mapping zip.
704      * @param baseDir A {@link File} pointing to the temp directory for base test mappings zip.
705      */
706     @VisibleForTesting
mergeTestMappingZips( IBuildInfo buildInfo, List<String> extraZips, File baseFile, File baseDir)707     void mergeTestMappingZips(
708             IBuildInfo buildInfo, List<String> extraZips, File baseFile, File baseDir)
709             throws IOException {
710         if (extraZips.isEmpty()) {
711             return;
712         }
713         Set<String> baseNames = getTestMappingSources(baseFile);
714         for (String zipName : extraZips) {
715             File zipFile;
716             if (buildInfo == null) {
717                 zipFile = lookupTestMappingZip(zipName);
718             } else {
719                 zipFile = buildInfo.getFile(zipName);
720             }
721             if (zipFile == null) {
722                 throw new HarnessRuntimeException(
723                         String.format("Missing %s in the BuildInfo file.", zipName),
724                         InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
725             }
726             Set<String> targetNames = getTestMappingSources(zipFile);
727             validateSources(baseNames, targetNames, zipName);
728             baseNames.addAll(targetNames);
729             if (zipFile.isDirectory()) {
730                 FileUtil.recursiveHardlink(zipFile, baseDir);
731             } else {
732                 ZipUtil2.extractZip(zipFile, baseDir);
733             }
734         }
735     }
736 
737     /**
738      * Helper to validate whether there exists collision of the path of Test Mapping files.
739      *
740      * @param base A {@link Set<String>} of the file paths.
741      * @param target A {@link Set<String>} of the file paths.
742      * @param zipName A {@link String} of the zip file path.
743      */
validateSources(Set<String> base, Set<String> target, String zipName)744     private void validateSources(Set<String> base, Set<String> target, String zipName) {
745         for (String name : target) {
746             if (base.contains(name)) {
747                 throw new HarnessRuntimeException(
748                     String.format("Collision of Test Mapping file: %s in artifact: %s.",
749                         name, zipName), InfraErrorIdentifier.TEST_MAPPING_PATH_COLLISION);
750             }
751         }
752     }
753 
754     /**
755      * Helper to collect the path of Test Mapping files with a given zip file.
756      *
757      * @param zipFile A {@link File} of the test mappings zip.
758      * @return A {@link Set<String>} for file paths from the test mappings zip.
759      */
760     @VisibleForTesting
getTestMappingSources(File zipFile)761     Set<String> getTestMappingSources(File zipFile) {
762         Set<String> fileNames = new HashSet<>();
763         if (zipFile.isDirectory()) {
764             Path zipFileDir = Paths.get(zipFile.getAbsolutePath());
765             try (Stream<Path> stream = Files.walk(zipFileDir, FileVisitOption.FOLLOW_LINKS)) {
766                 stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
767                         .forEach(
768                                 path ->
769                                         fileNames.add(
770                                                 zipFileDir
771                                                         .relativize(path.toAbsolutePath())
772                                                         .toString()));
773 
774             } catch (IOException e) {
775                 throw new RuntimeException(
776                         String.format(
777                                 "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
778                                 e.getMessage(), zipFile.getAbsolutePath()),
779                         e);
780             }
781         } else {
782             Enumeration<? extends ZipArchiveEntry> entries = null;
783             ZipFile f = null;
784             try {
785                 f = new ZipFile(zipFile);
786                 entries = f.getEntries();
787             } catch (IOException e) {
788                 throw new RuntimeException(
789                         String.format(
790                                 "IO exception (%s) when accessing test_mappings.zip (%s)",
791                                 e.getMessage(), zipFile),
792                         e);
793             } finally {
794                 ZipUtil2.closeZip(f);
795             }
796             while (entries.hasMoreElements()) {
797                 ZipArchiveEntry entry = entries.nextElement();
798                 // TODO: Temporarily exclude disabled-presubmit-test file. We'll need to revisit if
799                 // that file is used on the older branch/target, if no, remove that file.
800                 if (!entry.isDirectory()
801                         && !entry.getName().equals(DISABLED_PRESUBMIT_TESTS_FILE)) {
802                     fileNames.add(entry.getName());
803                 }
804             }
805         }
806         return fileNames;
807     }
808 
809     /**
810      * Helper to locate the test mapping zip file from environment.
811      *
812      * @param zipName The original name of a test mappings zip.
813      * @return The test mapping file, or throw if unable to find.
814      */
lookupTestMappingZip(String zipName)815     private File lookupTestMappingZip(String zipName) {
816         String directFile = System.getenv(TestDiscoveryInvoker.TEST_MAPPING_ZIP_FILE);
817         if (directFile != null && new File(directFile).exists()) {
818             return new File(directFile);
819         }
820         throw new HarnessRuntimeException(
821                 String.format("Unable to locate the test mapping zip file %s", zipName),
822                 InfraErrorIdentifier.TEST_MAPPING_FILE_NOT_EXIST);
823     }
824 }
825