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