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