OLD | NEW |
1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 from __future__ import print_function | 5 from __future__ import print_function |
6 | 6 |
7 import json | 7 import json |
8 import os | 8 import os |
9 import subprocess | 9 import subprocess |
10 import sys | 10 import sys |
11 | 11 |
12 from . import package | 12 from . import package |
13 | 13 |
14 | 14 |
| 15 NUL = open(os.devnull, 'w') |
| 16 |
15 ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') | 17 ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') |
16 | 18 |
17 | 19 |
18 def default_json_encode(o): | 20 def default_json_encode(o): |
19 """Fallback for objects that JSON library can't serialize.""" | 21 """Fallback for objects that JSON library can't serialize.""" |
20 if isinstance(o, package.CommitInfo): | 22 if isinstance(o, package.CommitInfo): |
21 return o.dump() | 23 return o.dump() |
22 | 24 |
23 return repr(o) | 25 return repr(o) |
24 | 26 |
25 | 27 |
26 def run_simulation_test(repo_root, package_spec, additional_args=None): | 28 # This is the path within the recipes-py repo to the per-repo recipes.py script. |
| 29 # Ideally we'd read this somehow from each candidate engine repo version, but |
| 30 # for now assume it lives in a fixed location within the engine. |
| 31 RECIPES_PY_REL_PATH = ('doc', 'recipes.py') |
| 32 |
| 33 # These are the lines to look for in doc/recipes.py as well as the target repo's |
| 34 # copy of that file. Any lines found between these lines will be replaced |
| 35 # verbatim in the new recipes.py file. |
| 36 EDIT_HEADER = '#### PER-REPO CONFIGURATION (editable) ####\n' |
| 37 EDIT_FOOTER = '#### END PER-REPO CONFIGURATION ####\n' |
| 38 |
| 39 |
| 40 def write_new_recipes_py(context, spec, repo_cfg_block): |
| 41 """Uses the doc/recipes.py script from the currently-checked-out version of |
| 42 the recipe_engine (in `context`) as a template, and writes it to the |
| 43 recipes_dir of the destination repo (also from `context`). Replaces the lines |
| 44 between the EDIT_HEADER and EDIT_FOOTER with the lines from repo_cfg_block, |
| 45 verbatim. |
| 46 |
| 47 Args: |
| 48 context (PackageContext) - The context of where to find the checked-out |
| 49 recipe_engine as well as where to put the new recipes.py. |
| 50 spec (PackageSpec) - The rolled spec (result of |
| 51 RollCandidate.get_rolled_spec()) |
| 52 repo_cfg_block (list(str)) - The list of lines (including newlines) |
| 53 extracted from the repo's original recipes.py file (using the |
| 54 extract_repo_cfg_block function). |
| 55 """ |
| 56 source_path = os.path.join(spec.deps['recipe_engine'].path, |
| 57 *RECIPES_PY_REL_PATH) |
| 58 dest_path = os.path.join(context.recipes_dir, 'recipes.py') |
| 59 with open(source_path, 'rb') as source: |
| 60 with open(dest_path, 'wb') as dest: |
| 61 for line in source: |
| 62 dest.write(line) |
| 63 if line == EDIT_HEADER: |
| 64 break |
| 65 dest.writelines(repo_cfg_block) |
| 66 for line in source: |
| 67 if line == EDIT_FOOTER: |
| 68 dest.write(line) |
| 69 break |
| 70 dest.writelines(source) |
| 71 if sys.platform != 'win32': |
| 72 os.chmod(dest_path, os.stat(dest_path).st_mode|0111) |
| 73 |
| 74 |
| 75 def extract_repo_cfg_block(context): |
| 76 """Extracts the lines between EDIT_HEADER and EDIT_FOOTER from the |
| 77 to-be-autorolled-repo's recipes.py file. |
| 78 |
| 79 Args: |
| 80 context (PackageContext) - The context of where to find the repo's current |
| 81 recipes.py file. |
| 82 |
| 83 Returns list(str) - The list of lines (including newlines) which occur between |
| 84 the EDIT_HEADER and EDIT_FOOTER in the repo's recipes.py file. |
| 85 """ |
| 86 recipes_py_path = os.path.join(context.recipes_dir, 'recipes.py') |
| 87 block = [] |
| 88 with open(recipes_py_path, 'rb') as f: |
| 89 in_section = False |
| 90 for line in f: |
| 91 if not in_section and line == EDIT_HEADER: |
| 92 in_section = True |
| 93 elif in_section: |
| 94 if line == EDIT_FOOTER: |
| 95 break |
| 96 block.append(line) |
| 97 if not block: |
| 98 raise ValueError('unable to find configuration section in %r' % |
| 99 (recipes_py_path,)) |
| 100 return block |
| 101 |
| 102 |
| 103 def fetch(repo_root, package_spec): |
| 104 """ |
| 105 Just fetch the recipes to the newly configured version. |
| 106 """ |
| 107 # Use _local_ recipes.py, so that it checks out the pinned recipe engine, |
| 108 # rather than running recipe engine which may be at a different revision |
| 109 # than the pinned one. |
| 110 args = [ |
| 111 sys.executable, |
| 112 os.path.join(repo_root, package_spec.recipes_path, 'recipes.py'), |
| 113 'fetch', |
| 114 ] |
| 115 subprocess.check_call(args, stdout=NUL, stderr=NUL) |
| 116 |
| 117 |
| 118 def run_simulation_test(repo_root, package_spec, additional_args=None, |
| 119 allow_fetch=False): |
27 """ | 120 """ |
28 Runs recipe simulation test for given package. | 121 Runs recipe simulation test for given package. |
29 | 122 |
30 Returns a tuple of exit code and output. | 123 Returns a tuple of exit code and output. |
31 """ | 124 """ |
32 # Use _local_ recipes.py, so that it checks out the pinned recipe engine, | 125 # Use _local_ recipes.py, so that it checks out the pinned recipe engine, |
33 # rather than running recipe engine which may be at a different revision | 126 # rather than running recipe engine which may be at a different revision |
34 # than the pinned one. | 127 # than the pinned one. |
35 args = [ | 128 args = [ |
36 sys.executable, | 129 sys.executable, |
37 os.path.join(repo_root, package_spec.recipes_path, 'recipes.py'), | 130 os.path.join(repo_root, package_spec.recipes_path, 'recipes.py'), |
38 'simulation_test', | |
39 ] | 131 ] |
| 132 if not allow_fetch: |
| 133 args.append('--no-fetch') |
| 134 args.append('simulation_test') |
40 if additional_args: | 135 if additional_args: |
41 args.extend(additional_args) | 136 args.extend(additional_args) |
42 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 137 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
43 output, _ = p.communicate() | 138 output, _ = p.communicate() |
44 rc = p.returncode | 139 rc = p.returncode |
45 return rc, output | 140 return rc, output |
46 | 141 |
47 | 142 |
48 def process_candidates(candidates, context, config_file, package_spec): | 143 def process_candidates(candidates, context, config_file, package_spec): |
49 roll_details = [] | 144 roll_details = [] |
50 trivial = None | 145 trivial = None |
51 picked_roll_details = None | 146 picked_roll_details = None |
52 | 147 |
| 148 repo_cfg_block = extract_repo_cfg_block(context) |
| 149 |
53 print('looking for a trivial roll...') | 150 print('looking for a trivial roll...') |
54 | 151 |
55 # Fill basic information about all the candidates. In later loops | 152 # Fill basic information about all the candidates. In later loops |
56 # we exit early depending on test results. | 153 # we exit early depending on test results. |
57 for candidate in candidates: | 154 for candidate in candidates: |
58 roll_details.append({ | 155 roll_details.append({ |
59 'spec': str(candidate.get_rolled_spec().dump()), | 156 'spec': str(candidate.get_rolled_spec().dump()), |
60 'diff': candidate.get_diff(), | 157 'diff': candidate.get_diff(), |
61 'commit_infos': candidate.get_commit_infos(), | 158 'commit_infos': candidate.get_commit_infos(), |
62 }) | 159 }) |
63 | 160 |
64 # Process candidates biggest first. If the roll is trivial, we want | 161 # Process candidates biggest first. If the roll is trivial, we want |
65 # the maximal one, e.g. to jump over some reverts, or include fixes | 162 # the maximal one, e.g. to jump over some reverts, or include fixes |
66 # landed later for incompatible API changes. | 163 # landed later for incompatible API changes. |
67 for i, candidate in enumerate(candidates): | 164 for i, candidate in enumerate(candidates): |
68 print(' processing candidate #%d... ' % (i + 1), end='') | 165 print(' processing candidate #%d... ' % (i + 1), end='') |
69 | 166 |
70 config_file.write(candidate.get_rolled_spec().dump()) | 167 spec = candidate.get_rolled_spec() |
| 168 config_file.write(spec.dump()) |
| 169 fetch(context.repo_root, package_spec) |
| 170 write_new_recipes_py(context, spec, repo_cfg_block) |
| 171 |
71 rc, output = run_simulation_test(context.repo_root, package_spec) | 172 rc, output = run_simulation_test(context.repo_root, package_spec) |
72 roll_details[i]['recipes_simulation_test'] = { | 173 roll_details[i]['recipes_simulation_test'] = { |
73 'output': output, | 174 'output': output, |
74 'rc': rc, | 175 'rc': rc, |
75 } | 176 } |
76 | 177 |
77 if rc == 0: | 178 if rc == 0: |
78 print('SUCCESS!') | 179 print('SUCCESS!') |
79 trivial = True | 180 trivial = True |
80 picked_roll_details = roll_details[i] | 181 picked_roll_details = roll_details[i] |
81 break | 182 break |
82 else: | 183 else: |
83 print('FAILED') | 184 print('FAILED') |
84 | 185 |
85 if not picked_roll_details: | 186 if not picked_roll_details: |
86 print('looking for a nontrivial roll...') | 187 print('looking for a nontrivial roll...') |
87 | 188 |
88 # Process candidates smallest first. If the roll is going to change | 189 # Process candidates smallest first. If the roll is going to change |
89 # expectations, it should be minimal to avoid pulling too many unrelated | 190 # expectations, it should be minimal to avoid pulling too many unrelated |
90 # changes. | 191 # changes. |
91 for i, candidate in reversed(list(enumerate(candidates))): | 192 for i, candidate in reversed(list(enumerate(candidates))): |
92 print(' processing candidate #%d... ' % (i + 1), end='') | 193 print(' processing candidate #%d... ' % (i + 1), end='') |
93 | 194 |
94 config_file.write(candidate.get_rolled_spec().dump()) | 195 spec = candidate.get_rolled_spec() |
| 196 config_file.write(spec.dump()) |
| 197 fetch(context.repo_root, package_spec) |
| 198 write_new_recipes_py(context, spec, repo_cfg_block) |
95 | 199 |
96 rc, output = run_simulation_test( | 200 rc, output = run_simulation_test( |
97 context.repo_root, package_spec, ['train']) | 201 context.repo_root, package_spec, ['train']) |
98 roll_details[i]['recipes_simulation_test_train'] = { | 202 roll_details[i]['recipes_simulation_test_train'] = { |
99 'output': output, | 203 'output': output, |
100 'rc': rc, | 204 'rc': rc, |
101 } | 205 } |
102 | 206 |
103 if rc == 0: | 207 if rc == 0: |
104 print('SUCCESS!') | 208 print('SUCCESS!') |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
157 | 261 |
158 results = {} | 262 results = {} |
159 try: | 263 try: |
160 results = test_rolls( | 264 results = test_rolls( |
161 config_file, context, package_spec, args.projects or []) | 265 config_file, context, package_spec, args.projects or []) |
162 finally: | 266 finally: |
163 if not results.get('success'): | 267 if not results.get('success'): |
164 # Restore initial state. Since we could be running simulation tests | 268 # Restore initial state. Since we could be running simulation tests |
165 # on other revisions, re-run them now as well. | 269 # on other revisions, re-run them now as well. |
166 config_file.write(package_spec.dump()) | 270 config_file.write(package_spec.dump()) |
167 run_simulation_test(context.repo_root, package_spec, ['train']) | 271 run_simulation_test(context.repo_root, package_spec, ['train'], |
| 272 allow_fetch=True) |
168 | 273 |
169 if args.output_json: | 274 if args.output_json: |
170 with open(args.output_json, 'w') as f: | 275 with open(args.output_json, 'w') as f: |
171 json.dump( | 276 json.dump( |
172 results, f, default=default_json_encode, sort_keys=True, indent=4) | 277 results, f, default=default_json_encode, sort_keys=True, indent=4) |
173 | 278 |
174 return 0 | 279 return 0 |
OLD | NEW |