| #!/usr/bin/env python |
| # Copyright 2015 gRPC authors. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Filter out tests based on file differences compared to merge target branch""" |
| |
| from __future__ import print_function |
| |
| import re |
| import six |
| from subprocess import check_output |
| |
| |
| class TestSuite: |
| """ |
| Contains label to identify job as belonging to this test suite and |
| triggers to identify if changed files are relevant |
| """ |
| |
| def __init__(self, labels): |
| """ |
| Build TestSuite to group tests based on labeling |
| :param label: strings that should match a jobs's platform, config, language, or test group |
| """ |
| self.triggers = [] |
| self.labels = labels |
| |
| def add_trigger(self, trigger): |
| """ |
| Add a regex to list of triggers that determine if a changed file should run tests |
| :param trigger: regex matching file relevant to tests |
| """ |
| self.triggers.append(trigger) |
| |
| |
| # Create test suites |
| _CORE_TEST_SUITE = TestSuite(['c']) |
| _CPP_TEST_SUITE = TestSuite(['c++']) |
| _CSHARP_TEST_SUITE = TestSuite(['csharp']) |
| _NODE_TEST_SUITE = TestSuite(['grpc-node']) |
| _OBJC_TEST_SUITE = TestSuite(['objc']) |
| _PHP_TEST_SUITE = TestSuite(['php', 'php7']) |
| _PYTHON_TEST_SUITE = TestSuite(['python']) |
| _RUBY_TEST_SUITE = TestSuite(['ruby']) |
| _LINUX_TEST_SUITE = TestSuite(['linux']) |
| _WINDOWS_TEST_SUITE = TestSuite(['windows']) |
| _MACOS_TEST_SUITE = TestSuite(['macos']) |
| _ALL_TEST_SUITES = [ |
| _CORE_TEST_SUITE, _CPP_TEST_SUITE, _CSHARP_TEST_SUITE, _NODE_TEST_SUITE, |
| _OBJC_TEST_SUITE, _PHP_TEST_SUITE, _PYTHON_TEST_SUITE, _RUBY_TEST_SUITE, |
| _LINUX_TEST_SUITE, _WINDOWS_TEST_SUITE, _MACOS_TEST_SUITE |
| ] |
| |
| # Dictionary of whitelistable files where the key is a regex matching changed files |
| # and the value is a list of tests that should be run. An empty list means that |
| # the changed files should not trigger any tests. Any changed file that does not |
| # match any of these regexes will trigger all tests |
| # DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU ARE DOING (be careful even if you do) |
| _WHITELIST_DICT = { |
| '^doc/': [], |
| '^examples/': [], |
| '^include/grpc\+\+/': [_CPP_TEST_SUITE], |
| '^summerofcode/': [], |
| '^src/cpp/': [_CPP_TEST_SUITE], |
| '^src/csharp/': [_CSHARP_TEST_SUITE], |
| '^src/objective\-c/': [_OBJC_TEST_SUITE], |
| '^src/php/': [_PHP_TEST_SUITE], |
| '^src/python/': [_PYTHON_TEST_SUITE], |
| '^src/ruby/': [_RUBY_TEST_SUITE], |
| '^templates/': [], |
| '^test/core/': [_CORE_TEST_SUITE, _CPP_TEST_SUITE], |
| '^test/cpp/': [_CPP_TEST_SUITE], |
| '^test/distrib/cpp/': [_CPP_TEST_SUITE], |
| '^test/distrib/csharp/': [_CSHARP_TEST_SUITE], |
| '^test/distrib/php/': [_PHP_TEST_SUITE], |
| '^test/distrib/python/': [_PYTHON_TEST_SUITE], |
| '^test/distrib/ruby/': [_RUBY_TEST_SUITE], |
| '^vsprojects/': [_WINDOWS_TEST_SUITE], |
| 'composer\.json$': [_PHP_TEST_SUITE], |
| 'config\.m4$': [_PHP_TEST_SUITE], |
| 'CONTRIBUTING\.md$': [], |
| 'Gemfile$': [_RUBY_TEST_SUITE], |
| 'grpc\.def$': [_WINDOWS_TEST_SUITE], |
| 'grpc\.gemspec$': [_RUBY_TEST_SUITE], |
| 'gRPC\.podspec$': [_OBJC_TEST_SUITE], |
| 'gRPC\-Core\.podspec$': [_OBJC_TEST_SUITE], |
| 'gRPC\-ProtoRPC\.podspec$': [_OBJC_TEST_SUITE], |
| 'gRPC\-RxLibrary\.podspec$': [_OBJC_TEST_SUITE], |
| 'BUILDING\.md$': [], |
| 'LICENSE$': [], |
| 'MANIFEST\.md$': [], |
| 'package\.json$': [_PHP_TEST_SUITE], |
| 'package\.xml$': [_PHP_TEST_SUITE], |
| 'PATENTS$': [], |
| 'PYTHON\-MANIFEST\.in$': [_PYTHON_TEST_SUITE], |
| 'README\.md$': [], |
| 'requirements\.txt$': [_PYTHON_TEST_SUITE], |
| 'setup\.cfg$': [_PYTHON_TEST_SUITE], |
| 'setup\.py$': [_PYTHON_TEST_SUITE] |
| } |
| |
| # Regex that combines all keys in _WHITELIST_DICT |
| _ALL_TRIGGERS = "(" + ")|(".join(_WHITELIST_DICT.keys()) + ")" |
| |
| # Add all triggers to their respective test suites |
| for trigger, test_suites in six.iteritems(_WHITELIST_DICT): |
| for test_suite in test_suites: |
| test_suite.add_trigger(trigger) |
| |
| |
| def _get_changed_files(base_branch): |
| """ |
| Get list of changed files between current branch and base of target merge branch |
| """ |
| # Get file changes between branch and merge-base of specified branch |
| # Not combined to be Windows friendly |
| base_commit = check_output(["git", "merge-base", base_branch, |
| "HEAD"]).rstrip() |
| return check_output(["git", "diff", base_commit, "--name-only", |
| "HEAD"]).splitlines() |
| |
| |
| def _can_skip_tests(file_names, triggers): |
| """ |
| Determines if tests are skippable based on if all files do not match list of regexes |
| :param file_names: list of changed files generated by _get_changed_files() |
| :param triggers: list of regexes matching file name that indicates tests should be run |
| :return: safe to skip tests |
| """ |
| for file_name in file_names: |
| if any(re.match(trigger, file_name) for trigger in triggers): |
| return False |
| return True |
| |
| |
| def _remove_irrelevant_tests(tests, skippable_labels): |
| """ |
| Filters out tests by config or language - will not remove sanitizer tests |
| :param tests: list of all tests generated by run_tests_matrix.py |
| :param skippable_labels: list of languages and platforms with skippable tests |
| :return: list of relevant tests |
| """ |
| # test.labels[0] is platform and test.labels[2] is language |
| # We skip a test if both are considered safe to skip |
| return [test for test in tests if test.labels[0] not in skippable_labels or \ |
| test.labels[2] not in skippable_labels] |
| |
| |
| def affects_c_cpp(base_branch): |
| """ |
| Determines if a pull request's changes affect C/C++. This function exists because |
| there are pull request tests that only test C/C++ code |
| :param base_branch: branch that a pull request is requesting to merge into |
| :return: boolean indicating whether C/C++ changes are made in pull request |
| """ |
| changed_files = _get_changed_files(base_branch) |
| # Run all tests if any changed file is not in the whitelist dictionary |
| for changed_file in changed_files: |
| if not re.match(_ALL_TRIGGERS, changed_file): |
| return True |
| return not _can_skip_tests( |
| changed_files, _CPP_TEST_SUITE.triggers + _CORE_TEST_SUITE.triggers) |
| |
| |
| def filter_tests(tests, base_branch): |
| """ |
| Filters out tests that are safe to ignore |
| :param tests: list of all tests generated by run_tests_matrix.py |
| :return: list of relevant tests |
| """ |
| print( |
| 'Finding file differences between gRPC %s branch and pull request...\n' |
| % base_branch) |
| changed_files = _get_changed_files(base_branch) |
| for changed_file in changed_files: |
| print(' %s' % changed_file) |
| print('') |
| |
| # Run all tests if any changed file is not in the whitelist dictionary |
| for changed_file in changed_files: |
| if not re.match(_ALL_TRIGGERS, changed_file): |
| return (tests) |
| # Figure out which language and platform tests to run |
| skippable_labels = [] |
| for test_suite in _ALL_TEST_SUITES: |
| if _can_skip_tests(changed_files, test_suite.triggers): |
| for label in test_suite.labels: |
| print(' %s tests safe to skip' % label) |
| skippable_labels.append(label) |
| tests = _remove_irrelevant_tests(tests, skippable_labels) |
| return tests |