-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathget_target_packages.py
More file actions
227 lines (182 loc) · 7.17 KB
/
get_target_packages.py
File metadata and controls
227 lines (182 loc) · 7.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# Copyright 2017 Google LLC
#
# 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.
"""Print a list of packages which require testing."""
import os
import re
import subprocess
import warnings
CURRENT_DIR = os.path.realpath(os.path.dirname(__file__))
BASE_DIR = os.path.realpath(os.path.join(CURRENT_DIR, '..', '..'))
GITHUB_REPO = os.environ.get('GITHUB_REPO', 'google-cloud-python')
CIRCLE_TAG = os.environ.get('CIRCLE_TAG')
# NOTE: This reg-ex is copied from ``get_tagged_packages``.
TAG_RE = re.compile(r"""
^
(?P<pkg>
(([a-z]+)-)*) # pkg-name-with-hyphens- (empty allowed)
([0-9]+)\.([0-9]+)\.([0-9]+) # Version x.y.z (x, y, z all ints)
$
""", re.VERBOSE)
# This is the current set of dependencies by package.
# As of this writing, the only "real" dependency is that of error_reporting
# (on logging), the rest are just system test dependencies.
PKG_DEPENDENCIES = {
'core': {'api_core'},
'bigquery': {'storage'},
'error_reporting': {'logging'},
'language': {'storage'},
'logging': {'bigquery', 'pubsub', 'storage'},
'speech': {'storage'},
'vision': {'storage'},
}
def get_baseline():
"""Return the baseline commit.
On a pull request, or on a branch, return the master tip.
Locally, return a value pulled from environment variables, or None if
the environment variables are not set.
On a push to master, return None. This will effectively cause everything
to be considered to be affected.
"""
# If this is a pull request or branch, return the tip for master.
# We will test only packages which have changed since that point.
ci_non_master = os.environ.get('CI', '') == 'true' and any([
os.environ.get('CIRCLE_BRANCH', '') != 'master',
os.environ.get('CIRCLE_PR_NUMBER', ''),
])
if ci_non_master:
repo_url = 'git@github.com:GoogleCloudPlatform/{}'.format(GITHUB_REPO)
subprocess.run(['git', 'remote', 'add', 'baseline', repo_url],
stderr=subprocess.DEVNULL)
subprocess.run(['git', 'pull', 'baseline'], stderr=subprocess.DEVNULL)
return 'baseline/master'
# If environment variables are set identifying what the master tip is,
# use that.
if os.environ.get('GOOGLE_CLOUD_TESTING_REMOTE', ''):
remote = os.environ['GOOGLE_CLOUD_TESTING_REMOTE']
branch = os.environ.get('GOOGLE_CLOUD_TESTING_BRANCH', 'master')
return '%s/%s' % (remote, branch)
# If we are not in CI and we got this far, issue a warning.
if not os.environ.get('CI', ''):
warnings.warn('No baseline could be determined; this means tests '
'will run for every package. If this is local '
'development, set the $GOOGLE_CLOUD_TESTING_REMOTE '
'environment variable.')
# That is all we can do; return None.
return None
def get_changed_files():
"""Return a list of files that have been changed since the baseline.
If there is no base, return None.
"""
# Get the baseline, and fail quickly if there is no baseline.
baseline = get_baseline()
if not baseline:
return None
# Return a list of altered files.
try:
return subprocess.check_output([
'git', 'diff', '--name-only', '{}..HEAD'.format(baseline),
], stderr=subprocess.DEVNULL).decode('utf8').strip().split('\n')
except subprocess.CalledProcessError:
warnings.warn('Unable to perform git diff; falling back to assuming '
'all packages have changed.')
return None
def reverse_map(dict_of_sets):
"""Reverse a map of one-to-many.
So the map::
{
'A': {'B', 'C'},
'B': {'C'},
}
becomes
{
'B': {'A'},
'C': {'A', 'B'},
}
Args:
dict_of_sets (dict[set]): A dictionary of sets, mapping
one value to many.
Returns:
dict[set]: The reversed map.
"""
result = {}
for key, values in dict_of_sets.items():
for value in values:
result.setdefault(value, set()).add(key)
return result
def get_changed_packages(file_list):
"""Return a list of changed packages based on the provided file list.
If the file list is None, then all packages should be considered to be
altered.
"""
# Determine a complete list of packages.
all_packages = set()
for file_ in os.listdir(BASE_DIR):
abs_file = os.path.realpath(os.path.join(BASE_DIR, file_))
nox_file = os.path.join(abs_file, 'nox.py')
if os.path.isdir(abs_file) and os.path.isfile(nox_file):
all_packages.add(file_)
# If ther is no file list, send down the full package set.
if file_list is None:
return all_packages
# Create a set based on the list of changed files.
answer = set()
reverse_deps = reverse_map(PKG_DEPENDENCIES)
for file_ in file_list:
# Ignore root directory changes (setup.py, .gitignore, etc.).
if os.path.sep not in file_:
continue
# Ignore changes that are not in a package (usually this will be docs).
package = file_.split(os.path.sep, 1)[0]
if package not in all_packages:
continue
# If there is a change in core, short-circuit now and return
# everything.
if package in ('api_core', 'core'):
return all_packages
# Add the package, as well as any dependencies this package has.
# NOTE: For now, dependencies only go down one level.
answer.add(package)
answer = answer.union(reverse_deps.get(package, set()))
# We got this far without being short-circuited; return the final answer.
return answer
def get_tagged_package():
"""Return the package corresponding to the current tag.
If there is not tag, will return :data:`None`.
"""
if CIRCLE_TAG is None:
return
match = TAG_RE.match(CIRCLE_TAG)
if match is None:
return
pkg_name = match.group('pkg')
if pkg_name == '':
# NOTE: This corresponds to the "umbrella" tag.
return
return pkg_name.rstrip('-').replace('-', '_')
def get_target_packages():
"""Return a list of target packages to be run in the current build.
If in a tag build, will run only the package(s) that are tagged, otherwise
will run the packages that have file changes in them (or packages that
depend on those).
"""
tagged_package = get_tagged_package()
if tagged_package is None:
file_list = get_changed_files()
for package in sorted(get_changed_packages(file_list)):
yield package
else:
yield tagged_package
if __name__ == '__main__':
for package in get_target_packages():
print(package)