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

Side by Side Diff: scripts/slave/unittests/recipes_test.py

Issue 23889036: Refactor the way that TestApi works so that it is actually useful. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: rebase Created 7 years, 3 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 | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Provides test coverage for individual recipes. 6 """Provides test coverage for individual recipes.
7 7
8 Recipe tests are located in ../recipes_test/*.py. 8 Recipe tests are located in ../recipes_test/*.py.
9 9
10 Each py file's splitext'd name is expected to match a recipe in ../recipes/*.py. 10 Each py file's splitext'd name is expected to match a recipe in ../recipes/*.py.
(...skipping 18 matching lines...) Expand all
29 If those files look right, make sure they get checked in with your changes. 29 If those files look right, make sure they get checked in with your changes.
30 30
31 When this file runs as a test (i.e. as `recipes_test.py`), it will re-evaluate 31 When this file runs as a test (i.e. as `recipes_test.py`), it will re-evaluate
32 the recipes using the test function input data and compare the result to the 32 the recipes using the test function input data and compare the result to the
33 values recorded in the .expected files. 33 values recorded in the .expected files.
34 34
35 Additionally, this test cannot pass unless every recipe in ../recipes has 100% 35 Additionally, this test cannot pass unless every recipe in ../recipes has 100%
36 code coverage when executed via the tests in ../recipes_test. 36 code coverage when executed via the tests in ../recipes_test.
37 """ 37 """
38 38
39 import collections
40 import contextlib 39 import contextlib
41 import json 40 import json
42 import os 41 import os
43 import sys 42 import sys
44 43
45 from glob import glob 44 from glob import glob
46 45
47 import test_env # pylint: disable=W0611 46 import test_env # pylint: disable=F0401,W0611
48 47
49 import coverage 48 import coverage
50 49
51 import common.python26_polyfill # pylint: disable=W0611 50 import common.python26_polyfill # pylint: disable=W0611
52 import unittest 51 import unittest
53 52
54 from common import annotator 53 from common import annotator
54 from slave import recipe_util
55 from slave import annotated_run
56 from slave import recipe_loader
55 57
56 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) 58 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
57 ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir, os.pardir, 59 ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir, os.pardir,
58 os.pardir)) 60 os.pardir))
59 SLAVE_DIR = os.path.join(ROOT_PATH, 'slave', 'fake_slave', 'build') 61 SLAVE_DIR = os.path.join(ROOT_PATH, 'slave', 'fake_slave', 'build')
60 INTERNAL_DIR = os.path.join(ROOT_PATH, os.pardir, 'build_internal')
61 BASE_DIRS = {
62 'Public': os.path.dirname(SCRIPT_PATH),
63 'Internal': os.path.join(INTERNAL_DIR, 'scripts', 'slave'),
64 }
65 # TODO(iannucci): Check for duplicate recipe names when we have more than one
66 # base_dir
67 62
68 COVERAGE = coverage.coverage( 63 BASE_DIRS = recipe_util.BASE_DIRS
69 include=([os.path.join(x, 'recipes', '*') for x in BASE_DIRS.values()]+ 64 COVERAGE = None
70 [os.path.join(SCRIPT_PATH, os.pardir, 'recipe_modules',
71 '*', 'api.py')])
72 )
73 65
74 66
75 @contextlib.contextmanager 67 @contextlib.contextmanager
76 def cover(): 68 def cover():
77 COVERAGE.start() 69 COVERAGE.start()
78 try: 70 try:
79 yield 71 yield
80 finally: 72 finally:
81 COVERAGE.stop() 73 COVERAGE.stop()
82 74
83 with cover():
84 from slave import annotated_run
85 from slave import recipe_api
86
87 class TestAPI(object):
88 @staticmethod
89 def properties_generic(**kwargs):
90 """
91 Merge kwargs into a typical buildbot properties blob, and return the blob.
92 """
93 ret = {
94 'blamelist': 'cool_dev1337@chromium.org,hax@chromium.org',
95 'blamelist_real': ['cool_dev1337@chromium.org', 'hax@chromium.org'],
96 'buildername': 'TestBuilder',
97 'buildnumber': 571,
98 'mastername': 'chromium.testing.master',
99 'slavename': 'TestSlavename',
100 'workdir': '/path/to/workdir/TestSlavename',
101 }
102 ret.update(kwargs)
103 return ret
104
105 @staticmethod
106 def properties_scheduled(**kwargs):
107 """
108 Merge kwargs into a typical buildbot properties blob for a job fired off
109 by a chrome/trunk svn scheduler, and return the blob.
110 """
111 ret = TestAPI.properties_generic(
112 branch='TestBranch',
113 project='',
114 repository='svn://svn-mirror.golo.chromium.org/chrome/trunk',
115 revision='204787',
116 )
117 ret.update(kwargs)
118 return ret
119
120 @staticmethod
121 def properties_tryserver(**kwargs):
122 """
123 Merge kwargs into a typical buildbot properties blob for a job fired off
124 by a rietveld tryjob on the tryserver, and return the blob.
125 """
126 ret = TestAPI.properties_generic(
127 branch='',
128 issue=12853011,
129 patchset=1,
130 project='chrome',
131 repository='',
132 requester='commit-bot@chromium.org',
133 revision='HEAD',
134 rietveld='https://chromiumcodereview.appspot.com',
135 root='src',
136 )
137 ret.update(kwargs)
138 return ret
139
140
141 def expected_for(recipe_path, test_name): 75 def expected_for(recipe_path, test_name):
142 root, name = os.path.split(recipe_path) 76 root, name = os.path.split(recipe_path)
143 name = os.path.splitext(name)[0] 77 name = os.path.splitext(name)[0]
144 expect_path = os.path.join(root, '%s.expected' % name) 78 expect_path = os.path.join(root, '%s.expected' % name)
145 if not os.path.isdir(expect_path): 79 if not os.path.isdir(expect_path):
146 os.makedirs(expect_path) 80 os.makedirs(expect_path)
147 return os.path.join(expect_path, test_name+'.json') 81 return os.path.join(expect_path, test_name+'.json')
148 82
149 83
150 def exec_test_file(recipe_path): 84 def exec_test_file(recipe_path):
151 gvars = {} 85 gvars = {}
152 with cover(): 86 with cover():
153 execfile(recipe_path, gvars) 87 execfile(recipe_path, gvars)
154 try: 88 try:
155 gen = gvars['GenTests'](TestAPI()) 89 test_api = recipe_loader.CreateTestApi(gvars['DEPS'])
90 gen = gvars['GenTests'](test_api)
156 except Exception, e: 91 except Exception, e:
157 print "Caught exception while processing %s: %s" % (recipe_path, e) 92 print "Caught exception while processing %s: %s" % (recipe_path, e)
158 raise 93 raise
159 try: 94 try:
160 while True: 95 while True:
161 with cover(): 96 with cover():
162 name, test_data = next(gen) 97 test_data = next(gen)
163 yield name, test_data 98 yield test_data
164 except StopIteration: 99 except StopIteration:
165 pass 100 pass
101 except:
102 print 'Exception while processing "%s"!' % recipe_path
103 raise
166 104
167 105
168 def execute_test_case(test_data, recipe_path, recipe_name): 106 def execute_test_case(test_data, recipe_path, recipe_name):
169 test_data = test_data.copy() 107 try:
170 props = test_data.pop('properties', {}).copy() 108 props = test_data.properties
171 td = test_data.pop('step_mocks', {}).copy() 109 props['recipe'] = recipe_name
172 props['recipe'] = recipe_name
173 110
174 mock_data = test_data.pop('mock', {}) 111 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w'))
175 mock_data = collections.defaultdict(lambda: collections.defaultdict(dict),
176 mock_data)
177 112
178 assert not test_data, 'Got leftover test data: %s' % test_data 113 def api(*args, **kwargs):
114 return recipe_loader.CreateRecipeApi(test_data=test_data, *args, **kwargs)
179 115
180 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w')) 116 with cover():
181
182 def api(*args, **kwargs):
183 return recipe_api.CreateRecipeApi(mocks=mock_data, *args, **kwargs)
184
185 with cover():
186 try:
187 step_data = annotated_run.run_steps( 117 step_data = annotated_run.run_steps(
188 stream, props, props, api, td).steps_ran.values() 118 stream, props, props, api, test_data).steps_ran.values()
189 return [s.step for s in step_data] 119 return [s.step for s in step_data]
190 except: 120 except:
191 print 'Exception while processing "%s"!' % recipe_path 121 print 'Exception while processing "%s"!' % recipe_path
192 raise 122 raise
193 123
194 124
195 def train_from_tests((recipe_path, recipe_name)): 125 def train_from_tests((recipe_path, recipe_name)):
196 for path in glob(expected_for(recipe_path, '*')): 126 for path in glob(expected_for(recipe_path, '*')):
197 os.unlink(path) 127 os.unlink(path)
198 128
199 for name, test_data in exec_test_file(recipe_path): 129 for test_data in exec_test_file(recipe_path):
200 steps = execute_test_case(test_data, recipe_path, recipe_name) 130 steps = execute_test_case(test_data, recipe_path, recipe_name)
201 expected_path = expected_for(recipe_path, name) 131 expected_path = expected_for(recipe_path, test_data.name)
202 print 'Writing', expected_path 132 print 'Writing', expected_path
203 with open(expected_path, 'wb') as f: 133 with open(expected_path, 'wb') as f:
204 json.dump(steps, f, sort_keys=True, indent=2, separators=(',', ': ')) 134 json.dump(steps, f, sort_keys=True, indent=2, separators=(',', ': '))
205 135
206 return True 136 return True
207 137
208 138
209 def load_tests(loader, _standard_tests, _pattern): 139 def load_tests(loader, _standard_tests, _pattern):
210 """This method is invoked by unittest.main's automatic testloader.""" 140 """This method is invoked by unittest.main's automatic testloader."""
211 def create_test_class((recipe_path, recipe_name)): 141 def create_test_class((recipe_path, recipe_name)):
212 class RecipeTest(unittest.TestCase): 142 class RecipeTest(unittest.TestCase):
213 @classmethod 143 @classmethod
214 def add_test_methods(cls): 144 def add_test_methods(cls):
215 for name, test_data in exec_test_file(recipe_path): 145 for test_data in exec_test_file(recipe_path):
216 expected_path = expected_for(recipe_path, name) 146 expected_path = expected_for(recipe_path, test_data.name)
217 def add_test(test_data, expected_path, recipe_name): 147 def add_test(test_data, expected_path, recipe_name):
218 def test_(self): 148 def test_(self):
219 steps = execute_test_case(test_data, recipe_path, recipe_name) 149 steps = execute_test_case(test_data, recipe_path, recipe_name)
220 # Roundtrip json to get same string encoding as load 150 # Roundtrip json to get same string encoding as load
221 steps = json.loads(json.dumps(steps)) 151 steps = json.loads(json.dumps(steps))
222 with open(expected_path, 'rb') as f: 152 with open(expected_path, 'rb') as f:
223 expected = json.load(f) 153 expected = json.load(f)
224 self.assertEqual(steps, expected) 154 self.assertEqual(steps, expected)
225 test_.__name__ += name 155 test_.__name__ += test_data.name
226 setattr(cls, test_.__name__, test_) 156 setattr(cls, test_.__name__, test_)
227 add_test(test_data, expected_path, recipe_name) 157 add_test(test_data, expected_path, recipe_name)
228 158
229 RecipeTest.add_test_methods() 159 RecipeTest.add_test_methods()
230 160
231 RecipeTest.__name__ += '_for_%s' % ( 161 RecipeTest.__name__ += '_for_%s' % (
232 os.path.splitext(os.path.basename(recipe_path))[0]) 162 os.path.splitext(os.path.basename(recipe_path))[0])
233 return RecipeTest 163 return RecipeTest
234 164
235 suite = unittest.TestSuite() 165 suite = unittest.TestSuite()
236 for test_class in map(create_test_class, loop_over_recipes()): 166 for test_class in map(create_test_class, recipe_loader.loop_over_recipes()):
237 suite.addTest(loader.loadTestsFromTestCase(test_class)) 167 suite.addTest(loader.loadTestsFromTestCase(test_class))
238 return suite 168 return suite
239 169
240 170
241 def find_recipes(path, predicate):
242 for root, _dirs, files in os.walk(path):
243 for recipe in (f for f in files if predicate(f)):
244 recipe_path = os.path.join(root, recipe)
245 yield recipe_path
246
247
248 def loop_over_recipes():
249 for _name, path in BASE_DIRS.iteritems():
250 recipe_dir = os.path.join(path, 'recipes')
251 for recipe in find_recipes(
252 recipe_dir, lambda f: f.endswith('.py') and f[0] != '_'):
253 yield recipe, recipe[len(recipe_dir)+1:-len('.py')]
254 module_dir = os.path.join(path, 'recipe_modules')
255 for recipe in find_recipes(
256 module_dir, lambda f: f.endswith('example.py')):
257 module_name = os.path.dirname(recipe)[len(module_dir)+1:]
258 yield recipe, '%s:example' % module_name
259
260
261 def main(argv): 171 def main(argv):
262 if not os.path.exists(SLAVE_DIR): 172 if not os.path.exists(SLAVE_DIR):
263 os.makedirs(SLAVE_DIR) 173 os.makedirs(SLAVE_DIR)
264 174
265 os.chdir(SLAVE_DIR) 175 os.chdir(SLAVE_DIR)
266 176
267 training = False 177 training = False
268 is_help = False 178 is_help = False
269 if '--help' in argv or '-h' in argv: 179 if '--help' in argv or '-h' in argv:
270 print 'Pass --train to enter training mode.' 180 print 'Pass --train to enter training mode.'
271 print 181 print
272 is_help = True 182 is_help = True
273 if '--train' in argv: 183 if '--train' in argv:
274 argv.remove('--train') 184 argv.remove('--train')
275 training = True 185 training = True
276 if '--external' in argv: 186 if '--external' in argv:
277 argv.remove('--external') 187 argv.remove('--external')
278 del BASE_DIRS['Internal'] 188 BASE_DIRS[:] = [d for d in BASE_DIRS if 'internal' not in d]
189 global COVERAGE
190 COVERAGE = coverage.coverage(
191 include=(
192 [os.path.join(x, '*') for x in recipe_util.RECIPE_DIRS()] +
193 [os.path.join(x, '*', '*api.py') for x in recipe_util.MODULE_DIRS()]
194 )
195 )
279 196
280 had_errors = False 197 had_errors = False
281 if training and not is_help: 198 if training and not is_help:
282 for result in map(train_from_tests, loop_over_recipes()): 199 for result in map(train_from_tests, recipe_loader.loop_over_recipes()):
283 had_errors = had_errors or result 200 had_errors = had_errors or result
284 if had_errors: 201 if had_errors:
285 break 202 break
286 203
287 retcode = 1 if had_errors else 0 204 retcode = 1 if had_errors else 0
288 205
289 if not training: 206 if not training:
290 try: 207 try:
291 unittest.main() 208 unittest.main()
292 except SystemExit as e: 209 except SystemExit as e:
293 retcode = e.code or retcode 210 retcode = e.code or retcode
294 211
295 if not is_help: 212 if not is_help:
296 total_covered = COVERAGE.report() 213 total_covered = COVERAGE.report()
297 if total_covered != 100.0: 214 if total_covered != 100.0:
298 print 'FATAL: Recipes are not at 100% coverage.' 215 print 'FATAL: Recipes are not at 100% coverage.'
299 retcode = retcode or 2 216 retcode = retcode or 2
300 217
301 if training: 218 if training:
302 test_env.print_coverage_warning() 219 test_env.print_coverage_warning()
303 220
304 return retcode 221 return retcode
305 222
306 223
307 if __name__ == '__main__': 224 if __name__ == '__main__':
308 sys.exit(main(sys.argv)) 225 sys.exit(main(sys.argv))
OLDNEW
« scripts/slave/recipes/v8.py ('K') | « scripts/slave/unittests/recipe_configs_test.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698