1#!/usr/bin/env python 2# Copyright 2015 gRPC authors. 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"""Filter out tests based on file differences compared to merge target branch""" 16 17from __future__ import print_function 18 19import re 20import six 21from subprocess import check_output 22 23 24class TestSuite: 25 """ 26 Contains label to identify job as belonging to this test suite and 27 triggers to identify if changed files are relevant 28 """ 29 30 def __init__(self, labels): 31 """ 32 Build TestSuite to group tests based on labeling 33 :param label: strings that should match a jobs's platform, config, language, or test group 34 """ 35 self.triggers = [] 36 self.labels = labels 37 38 def add_trigger(self, trigger): 39 """ 40 Add a regex to list of triggers that determine if a changed file should run tests 41 :param trigger: regex matching file relevant to tests 42 """ 43 self.triggers.append(trigger) 44 45 46# Create test suites 47_CORE_TEST_SUITE = TestSuite(['c']) 48_CPP_TEST_SUITE = TestSuite(['c++']) 49_CSHARP_TEST_SUITE = TestSuite(['csharp']) 50_NODE_TEST_SUITE = TestSuite(['grpc-node']) 51_OBJC_TEST_SUITE = TestSuite(['objc']) 52_PHP_TEST_SUITE = TestSuite(['php', 'php7']) 53_PYTHON_TEST_SUITE = TestSuite(['python']) 54_RUBY_TEST_SUITE = TestSuite(['ruby']) 55_LINUX_TEST_SUITE = TestSuite(['linux']) 56_WINDOWS_TEST_SUITE = TestSuite(['windows']) 57_MACOS_TEST_SUITE = TestSuite(['macos']) 58_ALL_TEST_SUITES = [ 59 _CORE_TEST_SUITE, _CPP_TEST_SUITE, _CSHARP_TEST_SUITE, _NODE_TEST_SUITE, 60 _OBJC_TEST_SUITE, _PHP_TEST_SUITE, _PYTHON_TEST_SUITE, _RUBY_TEST_SUITE, 61 _LINUX_TEST_SUITE, _WINDOWS_TEST_SUITE, _MACOS_TEST_SUITE 62] 63 64# Dictionary of whitelistable files where the key is a regex matching changed files 65# and the value is a list of tests that should be run. An empty list means that 66# the changed files should not trigger any tests. Any changed file that does not 67# match any of these regexes will trigger all tests 68# DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU ARE DOING (be careful even if you do) 69_WHITELIST_DICT = { 70 '^doc/': [], 71 '^examples/': [], 72 '^include/grpc\+\+/': [_CPP_TEST_SUITE], 73 '^include/grpcpp/': [_CPP_TEST_SUITE], 74 '^summerofcode/': [], 75 '^src/cpp/': [_CPP_TEST_SUITE], 76 '^src/csharp/': [_CSHARP_TEST_SUITE], 77 '^src/objective\-c/': [_OBJC_TEST_SUITE], 78 '^src/php/': [_PHP_TEST_SUITE], 79 '^src/python/': [_PYTHON_TEST_SUITE], 80 '^src/ruby/': [_RUBY_TEST_SUITE], 81 '^templates/': [], 82 '^test/core/': [_CORE_TEST_SUITE, _CPP_TEST_SUITE], 83 '^test/cpp/': [_CPP_TEST_SUITE], 84 '^test/distrib/cpp/': [_CPP_TEST_SUITE], 85 '^test/distrib/csharp/': [_CSHARP_TEST_SUITE], 86 '^test/distrib/php/': [_PHP_TEST_SUITE], 87 '^test/distrib/python/': [_PYTHON_TEST_SUITE], 88 '^test/distrib/ruby/': [_RUBY_TEST_SUITE], 89 '^vsprojects/': [_WINDOWS_TEST_SUITE], 90 'composer\.json$': [_PHP_TEST_SUITE], 91 'config\.m4$': [_PHP_TEST_SUITE], 92 'CONTRIBUTING\.md$': [], 93 'Gemfile$': [_RUBY_TEST_SUITE], 94 'grpc\.def$': [_WINDOWS_TEST_SUITE], 95 'grpc\.gemspec$': [_RUBY_TEST_SUITE], 96 'gRPC\.podspec$': [_OBJC_TEST_SUITE], 97 'gRPC\-Core\.podspec$': [_OBJC_TEST_SUITE], 98 'gRPC\-ProtoRPC\.podspec$': [_OBJC_TEST_SUITE], 99 'gRPC\-RxLibrary\.podspec$': [_OBJC_TEST_SUITE], 100 'BUILDING\.md$': [], 101 'LICENSE$': [], 102 'MANIFEST\.md$': [], 103 'package\.json$': [_PHP_TEST_SUITE], 104 'package\.xml$': [_PHP_TEST_SUITE], 105 'PATENTS$': [], 106 'PYTHON\-MANIFEST\.in$': [_PYTHON_TEST_SUITE], 107 'README\.md$': [], 108 'requirements\.txt$': [_PYTHON_TEST_SUITE], 109 'setup\.cfg$': [_PYTHON_TEST_SUITE], 110 'setup\.py$': [_PYTHON_TEST_SUITE] 111} 112 113# Regex that combines all keys in _WHITELIST_DICT 114_ALL_TRIGGERS = "(" + ")|(".join(_WHITELIST_DICT.keys()) + ")" 115 116# Add all triggers to their respective test suites 117for trigger, test_suites in six.iteritems(_WHITELIST_DICT): 118 for test_suite in test_suites: 119 test_suite.add_trigger(trigger) 120 121 122def _get_changed_files(base_branch): 123 """ 124 Get list of changed files between current branch and base of target merge branch 125 """ 126 # Get file changes between branch and merge-base of specified branch 127 # Not combined to be Windows friendly 128 base_commit = check_output(["git", "merge-base", base_branch, 129 "HEAD"]).rstrip() 130 return check_output(["git", "diff", base_commit, "--name-only", 131 "HEAD"]).splitlines() 132 133 134def _can_skip_tests(file_names, triggers): 135 """ 136 Determines if tests are skippable based on if all files do not match list of regexes 137 :param file_names: list of changed files generated by _get_changed_files() 138 :param triggers: list of regexes matching file name that indicates tests should be run 139 :return: safe to skip tests 140 """ 141 for file_name in file_names: 142 if any(re.match(trigger, file_name) for trigger in triggers): 143 return False 144 return True 145 146 147def _remove_irrelevant_tests(tests, skippable_labels): 148 """ 149 Filters out tests by config or language - will not remove sanitizer tests 150 :param tests: list of all tests generated by run_tests_matrix.py 151 :param skippable_labels: list of languages and platforms with skippable tests 152 :return: list of relevant tests 153 """ 154 # test.labels[0] is platform and test.labels[2] is language 155 # We skip a test if both are considered safe to skip 156 return [test for test in tests if test.labels[0] not in skippable_labels or \ 157 test.labels[2] not in skippable_labels] 158 159 160def affects_c_cpp(base_branch): 161 """ 162 Determines if a pull request's changes affect C/C++. This function exists because 163 there are pull request tests that only test C/C++ code 164 :param base_branch: branch that a pull request is requesting to merge into 165 :return: boolean indicating whether C/C++ changes are made in pull request 166 """ 167 changed_files = _get_changed_files(base_branch) 168 # Run all tests if any changed file is not in the whitelist dictionary 169 for changed_file in changed_files: 170 if not re.match(_ALL_TRIGGERS, changed_file): 171 return True 172 return not _can_skip_tests( 173 changed_files, _CPP_TEST_SUITE.triggers + _CORE_TEST_SUITE.triggers) 174 175 176def filter_tests(tests, base_branch): 177 """ 178 Filters out tests that are safe to ignore 179 :param tests: list of all tests generated by run_tests_matrix.py 180 :return: list of relevant tests 181 """ 182 print( 183 'Finding file differences between gRPC %s branch and pull request...\n' 184 % base_branch) 185 changed_files = _get_changed_files(base_branch) 186 for changed_file in changed_files: 187 print(' %s' % changed_file) 188 print('') 189 190 # Run all tests if any changed file is not in the whitelist dictionary 191 for changed_file in changed_files: 192 if not re.match(_ALL_TRIGGERS, changed_file): 193 return (tests) 194 # Figure out which language and platform tests to run 195 skippable_labels = [] 196 for test_suite in _ALL_TEST_SUITES: 197 if _can_skip_tests(changed_files, test_suite.triggers): 198 for label in test_suite.labels: 199 print(' %s tests safe to skip' % label) 200 skippable_labels.append(label) 201 tests = _remove_irrelevant_tests(tests, skippable_labels) 202 return tests 203