1"""Classify changes in Ansible code.""" 2from __future__ import (absolute_import, division, print_function) 3__metaclass__ = type 4 5import collections 6import os 7import re 8import time 9 10from . import types as t 11 12from .target import ( 13 walk_module_targets, 14 walk_integration_targets, 15 walk_units_targets, 16 walk_compile_targets, 17 walk_sanity_targets, 18 load_integration_prefixes, 19 analyze_integration_target_dependencies, 20) 21 22from .util import ( 23 display, 24 is_subdir, 25) 26 27from .import_analysis import ( 28 get_python_module_utils_imports, 29 get_python_module_utils_name, 30) 31 32from .csharp_import_analysis import ( 33 get_csharp_module_utils_imports, 34 get_csharp_module_utils_name, 35) 36 37from .powershell_import_analysis import ( 38 get_powershell_module_utils_imports, 39 get_powershell_module_utils_name, 40) 41 42from .config import ( 43 TestConfig, 44 IntegrationConfig, 45) 46 47from .metadata import ( 48 ChangeDescription, 49) 50 51from .data import ( 52 data_context, 53) 54 55FOCUSED_TARGET = '__focused__' 56 57 58def categorize_changes(args, paths, verbose_command=None): 59 """ 60 :type args: TestConfig 61 :type paths: list[str] 62 :type verbose_command: str 63 :rtype: ChangeDescription 64 """ 65 mapper = PathMapper(args) 66 67 commands = { 68 'sanity': set(), 69 'units': set(), 70 'integration': set(), 71 'windows-integration': set(), 72 'network-integration': set(), 73 } 74 75 focused_commands = collections.defaultdict(set) 76 77 deleted_paths = set() 78 original_paths = set() 79 additional_paths = set() 80 no_integration_paths = set() 81 82 for path in paths: 83 if not os.path.exists(path): 84 deleted_paths.add(path) 85 continue 86 87 original_paths.add(path) 88 89 dependent_paths = mapper.get_dependent_paths(path) 90 91 if not dependent_paths: 92 continue 93 94 display.info('Expanded "%s" to %d dependent file(s):' % (path, len(dependent_paths)), verbosity=2) 95 96 for dependent_path in dependent_paths: 97 display.info(dependent_path, verbosity=2) 98 additional_paths.add(dependent_path) 99 100 additional_paths -= set(paths) # don't count changed paths as additional paths 101 102 if additional_paths: 103 display.info('Expanded %d changed file(s) into %d additional dependent file(s).' % (len(paths), len(additional_paths))) 104 paths = sorted(set(paths) | additional_paths) 105 106 display.info('Mapping %d changed file(s) to tests.' % len(paths)) 107 108 none_count = 0 109 110 for path in paths: 111 tests = mapper.classify(path) 112 113 if tests is None: 114 focused_target = False 115 116 display.info('%s -> all' % path, verbosity=1) 117 tests = all_tests(args) # not categorized, run all tests 118 display.warning('Path not categorized: %s' % path) 119 else: 120 focused_target = tests.pop(FOCUSED_TARGET, False) and path in original_paths 121 122 tests = dict((key, value) for key, value in tests.items() if value) 123 124 if focused_target and not any('integration' in command for command in tests): 125 no_integration_paths.add(path) # path triggers no integration tests 126 127 if verbose_command: 128 result = '%s: %s' % (verbose_command, tests.get(verbose_command) or 'none') 129 130 # identify targeted integration tests (those which only target a single integration command) 131 if 'integration' in verbose_command and tests.get(verbose_command): 132 if not any('integration' in command for command in tests if command != verbose_command): 133 if focused_target: 134 result += ' (focused)' 135 136 result += ' (targeted)' 137 else: 138 result = '%s' % tests 139 140 if not tests.get(verbose_command): 141 # minimize excessive output from potentially thousands of files which do not trigger tests 142 none_count += 1 143 verbosity = 2 144 else: 145 verbosity = 1 146 147 if args.verbosity >= verbosity: 148 display.info('%s -> %s' % (path, result), verbosity=1) 149 150 for command, target in tests.items(): 151 commands[command].add(target) 152 153 if focused_target: 154 focused_commands[command].add(target) 155 156 if none_count > 0 and args.verbosity < 2: 157 display.notice('Omitted %d file(s) that triggered no tests.' % none_count) 158 159 for command in commands: 160 commands[command].discard('none') 161 162 if any(target == 'all' for target in commands[command]): 163 commands[command] = set(['all']) 164 165 commands = dict((c, sorted(commands[c])) for c in commands if commands[c]) 166 focused_commands = dict((c, sorted(focused_commands[c])) for c in focused_commands) 167 168 for command in commands: 169 if commands[command] == ['all']: 170 commands[command] = [] # changes require testing all targets, do not filter targets 171 172 changes = ChangeDescription() 173 changes.command = verbose_command 174 changes.changed_paths = sorted(original_paths) 175 changes.deleted_paths = sorted(deleted_paths) 176 changes.regular_command_targets = commands 177 changes.focused_command_targets = focused_commands 178 changes.no_integration_paths = sorted(no_integration_paths) 179 180 return changes 181 182 183class PathMapper: 184 """Map file paths to test commands and targets.""" 185 def __init__(self, args): 186 """ 187 :type args: TestConfig 188 """ 189 self.args = args 190 self.integration_all_target = get_integration_all_target(self.args) 191 192 self.integration_targets = list(walk_integration_targets()) 193 self.module_targets = list(walk_module_targets()) 194 self.compile_targets = list(walk_compile_targets()) 195 self.units_targets = list(walk_units_targets()) 196 self.sanity_targets = list(walk_sanity_targets()) 197 self.powershell_targets = [target for target in self.sanity_targets if os.path.splitext(target.path)[1] in ('.ps1', '.psm1')] 198 self.csharp_targets = [target for target in self.sanity_targets if os.path.splitext(target.path)[1] == '.cs'] 199 200 self.units_modules = set(target.module for target in self.units_targets if target.module) 201 self.units_paths = set(a for target in self.units_targets for a in target.aliases) 202 self.sanity_paths = set(target.path for target in self.sanity_targets) 203 204 self.module_names_by_path = dict((target.path, target.module) for target in self.module_targets) 205 self.integration_targets_by_name = dict((target.name, target) for target in self.integration_targets) 206 self.integration_targets_by_alias = dict((a, target) for target in self.integration_targets for a in target.aliases) 207 208 self.posix_integration_by_module = dict((m, target.name) for target in self.integration_targets 209 if 'posix/' in target.aliases for m in target.modules) 210 self.windows_integration_by_module = dict((m, target.name) for target in self.integration_targets 211 if 'windows/' in target.aliases for m in target.modules) 212 self.network_integration_by_module = dict((m, target.name) for target in self.integration_targets 213 if 'network/' in target.aliases for m in target.modules) 214 215 self.prefixes = load_integration_prefixes() 216 self.integration_dependencies = analyze_integration_target_dependencies(self.integration_targets) 217 218 self.python_module_utils_imports = {} # populated on first use to reduce overhead when not needed 219 self.powershell_module_utils_imports = {} # populated on first use to reduce overhead when not needed 220 self.csharp_module_utils_imports = {} # populated on first use to reduce overhead when not needed 221 222 self.paths_to_dependent_targets = {} 223 224 for target in self.integration_targets: 225 for path in target.needs_file: 226 if path not in self.paths_to_dependent_targets: 227 self.paths_to_dependent_targets[path] = set() 228 229 self.paths_to_dependent_targets[path].add(target) 230 231 def get_dependent_paths(self, path): 232 """ 233 :type path: str 234 :rtype: list[str] 235 """ 236 unprocessed_paths = set(self.get_dependent_paths_non_recursive(path)) 237 paths = set() 238 239 while unprocessed_paths: 240 queued_paths = list(unprocessed_paths) 241 paths |= unprocessed_paths 242 unprocessed_paths = set() 243 244 for queued_path in queued_paths: 245 new_paths = self.get_dependent_paths_non_recursive(queued_path) 246 247 for new_path in new_paths: 248 if new_path not in paths: 249 unprocessed_paths.add(new_path) 250 251 return sorted(paths) 252 253 def get_dependent_paths_non_recursive(self, path): 254 """ 255 :type path: str 256 :rtype: list[str] 257 """ 258 paths = self.get_dependent_paths_internal(path) 259 paths += [target.path + '/' for target in self.paths_to_dependent_targets.get(path, set())] 260 paths = sorted(set(paths)) 261 262 return paths 263 264 def get_dependent_paths_internal(self, path): 265 """ 266 :type path: str 267 :rtype: list[str] 268 """ 269 ext = os.path.splitext(os.path.split(path)[1])[1] 270 271 if is_subdir(path, data_context().content.module_utils_path): 272 if ext == '.py': 273 return self.get_python_module_utils_usage(path) 274 275 if ext == '.psm1': 276 return self.get_powershell_module_utils_usage(path) 277 278 if ext == '.cs': 279 return self.get_csharp_module_utils_usage(path) 280 281 if is_subdir(path, data_context().content.integration_targets_path): 282 return self.get_integration_target_usage(path) 283 284 return [] 285 286 def get_python_module_utils_usage(self, path): 287 """ 288 :type path: str 289 :rtype: list[str] 290 """ 291 if not self.python_module_utils_imports: 292 display.info('Analyzing python module_utils imports...') 293 before = time.time() 294 self.python_module_utils_imports = get_python_module_utils_imports(self.compile_targets) 295 after = time.time() 296 display.info('Processed %d python module_utils in %d second(s).' % (len(self.python_module_utils_imports), after - before)) 297 298 name = get_python_module_utils_name(path) 299 300 return sorted(self.python_module_utils_imports[name]) 301 302 def get_powershell_module_utils_usage(self, path): 303 """ 304 :type path: str 305 :rtype: list[str] 306 """ 307 if not self.powershell_module_utils_imports: 308 display.info('Analyzing powershell module_utils imports...') 309 before = time.time() 310 self.powershell_module_utils_imports = get_powershell_module_utils_imports(self.powershell_targets) 311 after = time.time() 312 display.info('Processed %d powershell module_utils in %d second(s).' % (len(self.powershell_module_utils_imports), after - before)) 313 314 name = get_powershell_module_utils_name(path) 315 316 return sorted(self.powershell_module_utils_imports[name]) 317 318 def get_csharp_module_utils_usage(self, path): 319 """ 320 :type path: str 321 :rtype: list[str] 322 """ 323 if not self.csharp_module_utils_imports: 324 display.info('Analyzing C# module_utils imports...') 325 before = time.time() 326 self.csharp_module_utils_imports = get_csharp_module_utils_imports(self.powershell_targets, self.csharp_targets) 327 after = time.time() 328 display.info('Processed %d C# module_utils in %d second(s).' % (len(self.csharp_module_utils_imports), after - before)) 329 330 name = get_csharp_module_utils_name(path) 331 332 return sorted(self.csharp_module_utils_imports[name]) 333 334 def get_integration_target_usage(self, path): 335 """ 336 :type path: str 337 :rtype: list[str] 338 """ 339 target_name = path.split('/')[3] 340 dependents = [os.path.join(data_context().content.integration_targets_path, target) + os.path.sep 341 for target in sorted(self.integration_dependencies.get(target_name, set()))] 342 343 return dependents 344 345 def classify(self, path): 346 """ 347 :type path: str 348 :rtype: dict[str, str] | None 349 """ 350 result = self._classify(path) 351 352 # run all tests when no result given 353 if result is None: 354 return None 355 356 # run sanity on path unless result specified otherwise 357 if path in self.sanity_paths and 'sanity' not in result: 358 result['sanity'] = path 359 360 return result 361 362 def _classify(self, path): # type: (str) -> t.Optional[t.Dict[str, str]] 363 """Return the classification for the given path.""" 364 if data_context().content.is_ansible: 365 return self._classify_ansible(path) 366 367 if data_context().content.collection: 368 return self._classify_collection(path) 369 370 return None 371 372 def _classify_common(self, path): # type: (str) -> t.Optional[t.Dict[str, str]] 373 """Return the classification for the given path using rules common to all layouts.""" 374 dirname = os.path.dirname(path) 375 filename = os.path.basename(path) 376 name, ext = os.path.splitext(filename) 377 378 minimal = {} 379 380 if os.path.sep not in path: 381 if filename in ( 382 'azure-pipelines.yml', 383 'shippable.yml', 384 ): 385 return all_tests(self.args) # test infrastructure, run all tests 386 387 if is_subdir(path, '.azure-pipelines'): 388 return all_tests(self.args) # test infrastructure, run all tests 389 390 if is_subdir(path, '.github'): 391 return minimal 392 393 if is_subdir(path, data_context().content.integration_targets_path): 394 if not os.path.exists(path): 395 return minimal 396 397 target = self.integration_targets_by_name.get(path.split('/')[3]) 398 399 if not target: 400 display.warning('Unexpected non-target found: %s' % path) 401 return minimal 402 403 if 'hidden/' in target.aliases: 404 return minimal # already expanded using get_dependent_paths 405 406 return { 407 'integration': target.name if 'posix/' in target.aliases else None, 408 'windows-integration': target.name if 'windows/' in target.aliases else None, 409 'network-integration': target.name if 'network/' in target.aliases else None, 410 FOCUSED_TARGET: True, 411 } 412 413 if is_subdir(path, data_context().content.integration_path): 414 if dirname == data_context().content.integration_path: 415 for command in ( 416 'integration', 417 'windows-integration', 418 'network-integration', 419 ): 420 if name == command and ext == '.cfg': 421 return { 422 command: self.integration_all_target, 423 } 424 425 if name == command + '.requirements' and ext == '.txt': 426 return { 427 command: self.integration_all_target, 428 } 429 430 return { 431 'integration': self.integration_all_target, 432 'windows-integration': self.integration_all_target, 433 'network-integration': self.integration_all_target, 434 } 435 436 if is_subdir(path, data_context().content.sanity_path): 437 return { 438 'sanity': 'all', # test infrastructure, run all sanity checks 439 } 440 441 if is_subdir(path, data_context().content.unit_path): 442 if path in self.units_paths: 443 return { 444 'units': path, 445 } 446 447 # changes to files which are not unit tests should trigger tests from the nearest parent directory 448 449 test_path = os.path.dirname(path) 450 451 while test_path: 452 if test_path + '/' in self.units_paths: 453 return { 454 'units': test_path + '/', 455 } 456 457 test_path = os.path.dirname(test_path) 458 459 if is_subdir(path, data_context().content.module_path): 460 module_name = self.module_names_by_path.get(path) 461 462 if module_name: 463 return { 464 'units': module_name if module_name in self.units_modules else None, 465 'integration': self.posix_integration_by_module.get(module_name) if ext == '.py' else None, 466 'windows-integration': self.windows_integration_by_module.get(module_name) if ext in ['.cs', '.ps1'] else None, 467 'network-integration': self.network_integration_by_module.get(module_name), 468 FOCUSED_TARGET: True, 469 } 470 471 return minimal 472 473 if is_subdir(path, data_context().content.module_utils_path): 474 if ext == '.cs': 475 return minimal # already expanded using get_dependent_paths 476 477 if ext == '.psm1': 478 return minimal # already expanded using get_dependent_paths 479 480 if ext == '.py': 481 return minimal # already expanded using get_dependent_paths 482 483 if is_subdir(path, data_context().content.plugin_paths['action']): 484 if ext == '.py': 485 if name.startswith('net_'): 486 network_target = 'network/.*_%s' % name[4:] 487 488 if any(re.search(r'^%s$' % network_target, alias) for alias in self.integration_targets_by_alias): 489 return { 490 'network-integration': network_target, 491 'units': 'all', 492 } 493 494 return { 495 'network-integration': self.integration_all_target, 496 'units': 'all', 497 } 498 499 if self.prefixes.get(name) == 'network': 500 network_platform = name 501 elif name.endswith('_config') and self.prefixes.get(name[:-7]) == 'network': 502 network_platform = name[:-7] 503 elif name.endswith('_template') and self.prefixes.get(name[:-9]) == 'network': 504 network_platform = name[:-9] 505 else: 506 network_platform = None 507 508 if network_platform: 509 network_target = 'network/%s/' % network_platform 510 511 if network_target in self.integration_targets_by_alias: 512 return { 513 'network-integration': network_target, 514 'units': 'all', 515 } 516 517 display.warning('Integration tests for "%s" not found.' % network_target, unique=True) 518 519 return { 520 'units': 'all', 521 } 522 523 if is_subdir(path, data_context().content.plugin_paths['connection']): 524 units_dir = os.path.join(data_context().content.unit_path, 'plugins', 'connection') 525 if name == '__init__': 526 return { 527 'integration': self.integration_all_target, 528 'windows-integration': self.integration_all_target, 529 'network-integration': self.integration_all_target, 530 'units': os.path.join(units_dir, ''), 531 } 532 533 units_path = os.path.join(units_dir, 'test_%s.py' % name) 534 535 if units_path not in self.units_paths: 536 units_path = None 537 538 integration_name = 'connection_%s' % name 539 540 if integration_name not in self.integration_targets_by_name: 541 integration_name = None 542 543 windows_integration_name = 'connection_windows_%s' % name 544 545 if windows_integration_name not in self.integration_targets_by_name: 546 windows_integration_name = None 547 548 # entire integration test commands depend on these connection plugins 549 550 if name in ['winrm', 'psrp']: 551 return { 552 'windows-integration': self.integration_all_target, 553 'units': units_path, 554 } 555 556 if name == 'local': 557 return { 558 'integration': self.integration_all_target, 559 'network-integration': self.integration_all_target, 560 'units': units_path, 561 } 562 563 if name == 'network_cli': 564 return { 565 'network-integration': self.integration_all_target, 566 'units': units_path, 567 } 568 569 if name == 'paramiko_ssh': 570 return { 571 'integration': integration_name, 572 'network-integration': self.integration_all_target, 573 'units': units_path, 574 } 575 576 # other connection plugins have isolated integration and unit tests 577 578 return { 579 'integration': integration_name, 580 'windows-integration': windows_integration_name, 581 'units': units_path, 582 } 583 584 if is_subdir(path, data_context().content.plugin_paths['doc_fragments']): 585 return { 586 'sanity': 'all', 587 } 588 589 if is_subdir(path, data_context().content.plugin_paths['inventory']): 590 if name == '__init__': 591 return all_tests(self.args) # broad impact, run all tests 592 593 # These inventory plugins are enabled by default (see INVENTORY_ENABLED). 594 # Without dedicated integration tests for these we must rely on the incidental coverage from other tests. 595 test_all = [ 596 'host_list', 597 'script', 598 'yaml', 599 'ini', 600 'auto', 601 ] 602 603 if name in test_all: 604 posix_integration_fallback = get_integration_all_target(self.args) 605 else: 606 posix_integration_fallback = None 607 608 target = self.integration_targets_by_name.get('inventory_%s' % name) 609 units_dir = os.path.join(data_context().content.unit_path, 'plugins', 'inventory') 610 units_path = os.path.join(units_dir, 'test_%s.py' % name) 611 612 if units_path not in self.units_paths: 613 units_path = None 614 615 return { 616 'integration': target.name if target and 'posix/' in target.aliases else posix_integration_fallback, 617 'windows-integration': target.name if target and 'windows/' in target.aliases else None, 618 'network-integration': target.name if target and 'network/' in target.aliases else None, 619 'units': units_path, 620 FOCUSED_TARGET: target is not None, 621 } 622 623 if (is_subdir(path, data_context().content.plugin_paths['terminal']) or 624 is_subdir(path, data_context().content.plugin_paths['cliconf']) or 625 is_subdir(path, data_context().content.plugin_paths['netconf'])): 626 if ext == '.py': 627 if name in self.prefixes and self.prefixes[name] == 'network': 628 network_target = 'network/%s/' % name 629 630 if network_target in self.integration_targets_by_alias: 631 return { 632 'network-integration': network_target, 633 'units': 'all', 634 } 635 636 display.warning('Integration tests for "%s" not found.' % network_target, unique=True) 637 638 return { 639 'units': 'all', 640 } 641 642 return { 643 'network-integration': self.integration_all_target, 644 'units': 'all', 645 } 646 647 return None 648 649 def _classify_collection(self, path): # type: (str) -> t.Optional[t.Dict[str, str]] 650 """Return the classification for the given path using rules specific to collections.""" 651 result = self._classify_common(path) 652 653 if result is not None: 654 return result 655 656 filename = os.path.basename(path) 657 dummy, ext = os.path.splitext(filename) 658 659 minimal = {} 660 661 if path.startswith('changelogs/'): 662 return minimal 663 664 if path.startswith('docs/'): 665 return minimal 666 667 if '/' not in path: 668 if path in ( 669 '.gitignore', 670 'COPYING', 671 'LICENSE', 672 'Makefile', 673 ): 674 return minimal 675 676 if ext in ( 677 '.in', 678 '.md', 679 '.rst', 680 '.toml', 681 '.txt', 682 ): 683 return minimal 684 685 return None 686 687 def _classify_ansible(self, path): # type: (str) -> t.Optional[t.Dict[str, str]] 688 """Return the classification for the given path using rules specific to Ansible.""" 689 if path.startswith('test/units/compat/'): 690 return { 691 'units': 'test/units/', 692 } 693 694 result = self._classify_common(path) 695 696 if result is not None: 697 return result 698 699 dirname = os.path.dirname(path) 700 filename = os.path.basename(path) 701 name, ext = os.path.splitext(filename) 702 703 minimal = {} 704 705 if path.startswith('bin/'): 706 return all_tests(self.args) # broad impact, run all tests 707 708 if path.startswith('changelogs/'): 709 return minimal 710 711 if path.startswith('contrib/'): 712 return { 713 'units': 'test/units/contrib/' 714 } 715 716 if path.startswith('docs/'): 717 return minimal 718 719 if path.startswith('examples/'): 720 if path == 'examples/scripts/ConfigureRemotingForAnsible.ps1': 721 return { 722 'windows-integration': 'connection_winrm', 723 } 724 725 return minimal 726 727 if path.startswith('hacking/'): 728 return minimal 729 730 if path.startswith('lib/ansible/executor/powershell/'): 731 units_path = 'test/units/executor/powershell/' 732 733 if units_path not in self.units_paths: 734 units_path = None 735 736 return { 737 'windows-integration': self.integration_all_target, 738 'units': units_path, 739 } 740 741 if path.startswith('lib/ansible/'): 742 return all_tests(self.args) # broad impact, run all tests 743 744 if path.startswith('licenses/'): 745 return minimal 746 747 if path.startswith('packaging/'): 748 if path.startswith('packaging/requirements/'): 749 if name.startswith('requirements-') and ext == '.txt': 750 component = name.split('-', 1)[1] 751 752 candidates = ( 753 'cloud/%s/' % component, 754 ) 755 756 for candidate in candidates: 757 if candidate in self.integration_targets_by_alias: 758 return { 759 'integration': candidate, 760 } 761 762 return all_tests(self.args) # broad impact, run all tests 763 764 return minimal 765 766 if path.startswith('test/ansible_test/'): 767 return minimal # these tests are not invoked from ansible-test 768 769 if path.startswith('test/legacy/'): 770 return minimal 771 772 if path.startswith('test/lib/ansible_test/config/'): 773 if name.startswith('cloud-config-'): 774 cloud_target = 'cloud/%s/' % name.split('-')[2].split('.')[0] 775 776 if cloud_target in self.integration_targets_by_alias: 777 return { 778 'integration': cloud_target, 779 } 780 781 if path.startswith('test/lib/ansible_test/_data/completion/'): 782 if path == 'test/lib/ansible_test/_data/completion/docker.txt': 783 return all_tests(self.args, force=True) # force all tests due to risk of breaking changes in new test environment 784 785 if path.startswith('test/lib/ansible_test/_internal/cloud/'): 786 cloud_target = 'cloud/%s/' % name 787 788 if cloud_target in self.integration_targets_by_alias: 789 return { 790 'integration': cloud_target, 791 } 792 793 return all_tests(self.args) # test infrastructure, run all tests 794 795 if path.startswith('test/lib/ansible_test/_internal/sanity/'): 796 return { 797 'sanity': 'all', # test infrastructure, run all sanity checks 798 } 799 800 if path.startswith('test/lib/ansible_test/_data/sanity/'): 801 return { 802 'sanity': 'all', # test infrastructure, run all sanity checks 803 } 804 805 if path.startswith('test/lib/ansible_test/_internal/units/'): 806 return { 807 'units': 'all', # test infrastructure, run all unit tests 808 } 809 810 if path.startswith('test/lib/ansible_test/_data/units/'): 811 return { 812 'units': 'all', # test infrastructure, run all unit tests 813 } 814 815 if path.startswith('test/lib/ansible_test/_data/pytest/'): 816 return { 817 'units': 'all', # test infrastructure, run all unit tests 818 } 819 820 if path.startswith('test/lib/ansible_test/_data/requirements/'): 821 if name in ( 822 'integration', 823 'network-integration', 824 'windows-integration', 825 ): 826 return { 827 name: self.integration_all_target, 828 } 829 830 if name in ( 831 'sanity', 832 'units', 833 ): 834 return { 835 name: 'all', 836 } 837 838 if name.startswith('integration.cloud.'): 839 cloud_target = 'cloud/%s/' % name.split('.')[2] 840 841 if cloud_target in self.integration_targets_by_alias: 842 return { 843 'integration': cloud_target, 844 } 845 846 if path.startswith('test/lib/'): 847 return all_tests(self.args) # test infrastructure, run all tests 848 849 if path.startswith('test/utils/shippable/tools/'): 850 return minimal # not used by tests 851 852 if path.startswith('test/utils/shippable/'): 853 if dirname == 'test/utils/shippable': 854 test_map = { 855 'cloud.sh': 'integration:cloud/', 856 'freebsd.sh': 'integration:all', 857 'linux.sh': 'integration:all', 858 'network.sh': 'network-integration:all', 859 'osx.sh': 'integration:all', 860 'rhel.sh': 'integration:all', 861 'sanity.sh': 'sanity:all', 862 'units.sh': 'units:all', 863 'windows.sh': 'windows-integration:all', 864 } 865 866 test_match = test_map.get(filename) 867 868 if test_match: 869 test_command, test_target = test_match.split(':') 870 871 return { 872 test_command: test_target, 873 } 874 875 cloud_target = 'cloud/%s/' % name 876 877 if cloud_target in self.integration_targets_by_alias: 878 return { 879 'integration': cloud_target, 880 } 881 882 return all_tests(self.args) # test infrastructure, run all tests 883 884 if path.startswith('test/utils/'): 885 return minimal 886 887 if '/' not in path: 888 if path in ( 889 '.gitattributes', 890 '.gitignore', 891 '.mailmap', 892 'COPYING', 893 'Makefile', 894 ): 895 return minimal 896 897 if path in ( 898 'setup.py', 899 ): 900 return all_tests(self.args) # broad impact, run all tests 901 902 if ext in ( 903 '.in', 904 '.md', 905 '.rst', 906 '.toml', 907 '.txt', 908 ): 909 return minimal 910 911 return None # unknown, will result in fall-back to run all tests 912 913 914def all_tests(args, force=False): 915 """ 916 :type args: TestConfig 917 :type force: bool 918 :rtype: dict[str, str] 919 """ 920 if force: 921 integration_all_target = 'all' 922 else: 923 integration_all_target = get_integration_all_target(args) 924 925 return { 926 'sanity': 'all', 927 'units': 'all', 928 'integration': integration_all_target, 929 'windows-integration': integration_all_target, 930 'network-integration': integration_all_target, 931 } 932 933 934def get_integration_all_target(args): 935 """ 936 :type args: TestConfig 937 :rtype: str 938 """ 939 if isinstance(args, IntegrationConfig): 940 return args.changed_all_target 941 942 return 'all' 943