| 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 |