Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(126)

Side by Side Diff: recipe_engine/autoroll.py

Issue 2756503003: [autoroll] make autoroller propagate changes to recipes.py. (Closed)
Patch Set: rebase Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « doc/recipes.py ('k') | unittests/autoroll_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
OLDNEW
« no previous file with comments | « doc/recipes.py ('k') | unittests/autoroll_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698