• 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         } finally {
534             if (delete) {
535                 FileUtil.recursiveDelete(testMappingsDir);
536             }
537         }
538         return tests;
539     }
540 
541     /**
542      * Helper to get all TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
543      *
544      * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
545      * @return A {@code Set<Path>} of all the TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
546      */
547     @VisibleForTesting
getAllTestMappingPaths(Path testMappingsRootPath)548     Set<Path> getAllTestMappingPaths(Path testMappingsRootPath) {
549         Set<Path> allTestMappingPaths = new LinkedHashSet<>();
550         for (String path : mTestMappingRelativePaths) {
551             boolean hasAdded = false;
552             Path testMappingPath = testMappingsRootPath.resolve(path);
553             // Recursively find the TEST_MAPPING file until reaching to testMappingsRootPath.
554             while (!testMappingPath.equals(testMappingsRootPath)) {
555                 if (testMappingPath.resolve(TEST_MAPPING).toFile().exists()) {
556                     hasAdded = true;
557                     CLog.d("Adding TEST_MAPPING path: %s", testMappingPath);
558                     allTestMappingPaths.add(testMappingPath.resolve(TEST_MAPPING));
559                 }
560                 testMappingPath = testMappingPath.getParent();
561             }
562             if (!hasAdded) {
563                 CLog.w("Couldn't find TEST_MAPPING files from %s", path);
564             }
565         }
566         if (allTestMappingPaths.isEmpty()) {
567             throw new RuntimeException(
568                     String.format(
569                             "Couldn't find TEST_MAPPING files from %s", mTestMappingRelativePaths));
570         }
571         CLog.d("All resolved TEST_MAPPING paths: %s", allTestMappingPaths);
572         return allTestMappingPaths;
573     }
574 
575     /**
576      * Helper to find all tests in the TEST_MAPPING files from a given directory.
577      *
578      * @param testMappingsDir the {@link File} the directory containing all Test Mapping files.
579      * @return A {@code Map<String, Set<TestInfo>>} of tests in the given directory and its child
580      *     directories.
581      */
getAllTests(File testMappingsDir)582     public Map<String, Set<TestInfo>> getAllTests(File testMappingsDir) {
583         Map<String, Set<TestInfo>> allTests = new HashMap<String, Set<TestInfo>>();
584         Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
585         try (Stream<Path> stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS)) {
586             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
587                     .forEach(
588                             path ->
589                                     getAllTests(allTests, path, testMappingsRootPath));
590 
591         } catch (IOException e) {
592             throw new RuntimeException(
593                     String.format(
594                             "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
595                             e.getMessage(), testMappingsDir.getAbsolutePath()),
596                     e);
597         }
598         return allTests;
599     }
600 
601     /**
602      * Helper to find all tests in the TEST_MAPPING files from a given directory.
603      *
604      * @param allTests the {@code HashMap<String, Set<TestInfo>>} containing the tests of each test
605      *     group.
606      * @param path the {@link Path} to a TEST_MAPPING file.
607      * @param testMappingsRootPath the {@link Path} to a test mappings zip path.
608      */
getAllTests( Map<String, Set<TestInfo>> allTests, Path path, Path testMappingsRootPath)609     private void getAllTests(
610             Map<String, Set<TestInfo>> allTests, Path path, Path testMappingsRootPath) {
611         Map<String, Set<TestInfo>> testCollection =
612                 getTestCollection(path, testMappingsRootPath, new HashSet<>());
613         for (String group : testCollection.keySet()) {
614             allTests.computeIfAbsent(group, k -> new HashSet<>()).addAll(testCollection.get(group));
615         }
616     }
617 
618     /**
619      * Extract a zip file and return the directory that contains the content of unzipped files.
620      *
621      * @param testMappingsZip A {@link File} of the test mappings zip to extract.
622      * @return a {@link File} pointing to the temp directory for test mappings zip.
623      */
extractTestMappingsZip(File testMappingsZip)624     public static File extractTestMappingsZip(File testMappingsZip) {
625         File testMappingsDir = null;
626         try (CloseableTraceScope ignored = new CloseableTraceScope("extractTestMappingsZip")) {
627             testMappingsDir = ZipUtil2.extractZipToTemp(testMappingsZip, TEST_MAPPINGS_ZIP);
628         } catch (IOException e) {
629             throw new RuntimeException(
630                     String.format(
631                             "IO exception (%s) when extracting test mappings zip (%s)",
632                             e.getMessage(), testMappingsZip.getAbsolutePath()), e);
633         }
634         return testMappingsDir;
635     }
636 
637     /**
638      * Get disabled tests from test mapping artifact.
639      *
640      * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
641      * @param testGroup a {@link String} of the test group.
642      * @return a {@link Set<String>} containing all the disabled presubmit tests. No test is
643      *     returned if the testGroup is not PRESUBMIT.
644      */
645     @VisibleForTesting
getDisabledTests(Path testMappingsRootPath, String testGroup)646     Set<String> getDisabledTests(Path testMappingsRootPath, String testGroup) {
647         Set<String> disabledTests = new HashSet<>();
648         File disabledPresubmitTestsFile =
649                 new File(testMappingsRootPath.toString(), DISABLED_PRESUBMIT_TESTS_FILE);
650         if (!(testGroup.equals(PRESUBMIT) && disabledPresubmitTestsFile.exists())) {
651             return disabledTests;
652         }
653         try {
654             disabledTests.addAll(
655                     Arrays.asList(
656                             FileUtil.readStringFromFile(disabledPresubmitTestsFile)
657                                     .split("\\r?\\n")));
658         } catch (IOException e) {
659             throw new RuntimeException(
660                     String.format(
661                             "IO exception (%s) when reading disabled tests from file (%s)",
662                             e.getMessage(), disabledPresubmitTestsFile.getAbsolutePath()), e);
663         }
664         return disabledTests;
665     }
666 
667     /**
668      * Helper to get the matcher for parameterized mainline tests.
669      *
670      * @param {@code Set<TestInfo>} of tests set in the build artifact, test_mappings.zip.
671      * @return A {@link Matcher} for parameterized mainline tests.
672      */
getMainlineTestModuleName(TestInfo info)673     public static Matcher getMainlineTestModuleName(TestInfo info) throws ConfigurationException {
674         Matcher matcher = MAINLINE_REGEX.matcher(info.getName());
675         if (matcher.find()) {
676             return matcher;
677         }
678         throw new ConfigurationException(
679                 String.format(
680                         "Unmatched \"[]\" for \"%s\" configured in the %s. "
681                                 + "Parameter must contain square brackets.",
682                         info.getName(), info.getSources()));
683     }
684 
685     /**
686      * Merge additional test mapping zips into the given directory.
687      *
688      * @param buildInfo the {@link IBuildInfo} describing the build.
689      * @param extraZips A {@link List<String>} of additional zip file paths.
690      * @param baseFile A {@link File} of base test mapping zip.
691      * @param baseDir A {@link File} pointing to the temp directory for base test mappings zip.
692      */
693     @VisibleForTesting
mergeTestMappingZips( IBuildInfo buildInfo, List<String> extraZips, File baseFile, File baseDir)694     void mergeTestMappingZips(
695             IBuildInfo buildInfo, List<String> extraZips, File baseFile, File baseDir)
696             throws IOException {
697         if (extraZips.isEmpty()) {
698             return;
699         }
700         Set<String> baseNames = getTestMappingSources(baseFile);
701         for (String zipName : extraZips) {
702             File zipFile;
703             if (buildInfo == null) {
704                 zipFile = lookupTestMappingZip(zipName);
705             } else {
706                 zipFile = buildInfo.getFile(zipName);
707             }
708             if (zipFile == null) {
709                 throw new HarnessRuntimeException(
710                         String.format("Missing %s in the BuildInfo file.", zipName),
711                         InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
712             }
713             Set<String> targetNames = getTestMappingSources(zipFile);
714             validateSources(baseNames, targetNames, zipName);
715             baseNames.addAll(targetNames);
716             ZipUtil2.extractZip(zipFile, baseDir);
717         }
718     }
719 
720     /**
721      * Helper to validate whether there exists collision of the path of Test Mapping files.
722      *
723      * @param base A {@link Set<String>} of the file paths.
724      * @param target A {@link Set<String>} of the file paths.
725      * @param zipName A {@link String} of the zip file path.
726      */
validateSources(Set<String> base, Set<String> target, String zipName)727     private void validateSources(Set<String> base, Set<String> target, String zipName) {
728         for (String name : target) {
729             if (base.contains(name)) {
730                 throw new HarnessRuntimeException(
731                     String.format("Collision of Test Mapping file: %s in artifact: %s.",
732                         name, zipName), InfraErrorIdentifier.TEST_MAPPING_PATH_COLLISION);
733             }
734         }
735     }
736 
737     /**
738      * Helper to collect the path of Test Mapping files with a given zip file.
739      *
740      * @param zipFile A {@link File} of the test mappings zip.
741      * @return A {@link Set<String>} for file paths from the test mappings zip.
742      */
743     @VisibleForTesting
getTestMappingSources(File zipFile)744     Set<String> getTestMappingSources(File zipFile) {
745         Set<String> fileNames = new HashSet<>();
746         Enumeration<? extends ZipArchiveEntry> entries = null;
747         ZipFile f = null;
748         try {
749             f = new ZipFile(zipFile);
750             entries = f.getEntries();
751         } catch (IOException e) {
752             throw new RuntimeException(
753                     String.format(
754                             "IO exception (%s) when accessing test_mappings.zip (%s)",
755                             e.getMessage(), zipFile),
756                     e);
757         } finally {
758             ZipUtil2.closeZip(f);
759         }
760         while (entries.hasMoreElements()) {
761             ZipArchiveEntry entry = entries.nextElement();
762             // TODO: Temporarily exclude disabled-presubmit-test file. We'll need to revisit if that
763             // file is used on the older branch/target, if no, remove that file.
764             if (!entry.isDirectory() && !entry.getName().equals(DISABLED_PRESUBMIT_TESTS_FILE)) {
765                 fileNames.add(entry.getName());
766             }
767         }
768         return fileNames;
769     }
770 
771     /**
772      * Helper to locate the test mapping zip file from environment.
773      *
774      * @param zipName The original name of a test mappings zip.
775      * @return The test mapping file, or throw if unable to find.
776      */
lookupTestMappingZip(String zipName)777     private File lookupTestMappingZip(String zipName) {
778         String directFile = System.getenv(TestDiscoveryInvoker.TEST_MAPPING_ZIP_FILE);
779         if (directFile != null && new File(directFile).exists()) {
780             return new File(directFile);
781         }
782         throw new HarnessRuntimeException(
783                 String.format("Unable to locate the test mapping zip file %s", zipName),
784                 InfraErrorIdentifier.TEST_MAPPING_FILE_NOT_EXIST);
785     }
786 }
787