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

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

Issue 14988009: First cut of testing infrastructure for recipes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Add coverage to third_party. The C module is a speed-only optimization. Created 7 years, 7 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
(Empty)
1 #!/usr/bin/env python
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
4 # found in the LICENSE file.
5
6 import contextlib
7 import json
8 import os
9 import sys
10 import unittest
11
12 from glob import glob
13
14 import test_env # pylint: disable=W0611
15
16 import coverage
17
18 from common import annotator
19 from slave import annotated_run
20 from slave import recipe_util
21
22 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
23 ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir, os.pardir,
24 os.pardir))
25 SLAVE_DIR = os.path.join(ROOT_PATH, 'slave', 'fake_slave', 'build')
26 BASE_DIRS = {
27 'Public': os.path.dirname(SCRIPT_PATH)
28 }
29 # TODO(iannucci): Check for duplicate recipe names when we have more than one
30 # base_dir
31
32 COVERAGE = coverage.coverage(
33 include=[os.path.join(x, 'recipes', '*') for x in BASE_DIRS.values()])
34 @contextlib.contextmanager
Vadim Shtayura 2013/05/11 00:33:32 Add newline above this line.
iannucci 2013/05/11 04:12:27 Done.
35 def cover():
36 COVERAGE.start()
37 try:
38 yield
39 finally:
40 COVERAGE.stop()
41
42
43 class TestAPI(object):
44 @staticmethod
45 def tryserver_build_properties(**kwargs):
46 ret = {
47 'issue': 12853011,
48 'patchset': 1,
49 'description': 'This is a test',
50 'blamelist': ['cool_dev1337@chromium.org'],
51 'rietveld': 'https://chromiumcodereview.appspot.com',
52 }
53 ret.update(kwargs)
54 return ret
55
56
57 class DevNull(object):
58 @staticmethod
59 def write(data):
60 pass
61
62 @staticmethod
63 def flush():
64 pass
65
66
67 def test_path_for_recipe(recipe_path):
68 root = os.path.dirname(os.path.dirname(recipe_path))
69 return os.path.join(root, 'recipes_test', os.path.basename(recipe_path))
70
71
72 def has_test(recipe_path):
73 return os.path.exists(test_path_for_recipe(recipe_path))
74
75
76 def expected_for(recipe_path, test_name):
77 test_base = os.path.splitext(test_path_for_recipe(recipe_path))[0]
78 return "%s.%s.expected" % (test_base, test_name)
79
80
81 def exec_test_file(recipe_path):
82 test_path = test_path_for_recipe(recipe_path)
83 gvars = {}
84 execfile(test_path, gvars)
85 return dict(item for item in gvars.iteritems() if item[0].endswith('_test'))
86
87
88 def execute_test_case(test_fn, recipe_path):
89 test_data = test_fn(TestAPI())
90 bp = test_data.get('build_properties', {})
91 fp = test_data.get('factory_properties', {})
92 fp['recipe'] = os.path.basename(os.path.splitext(recipe_path)[0])
93
94 with cover():
95 with recipe_util.mock_paths():
96 return annotated_run.make_steps(
97 annotator.StructuredAnnotationStream(stream=DevNull()), bp, fp, True)
Vadim Shtayura 2013/05/11 00:33:32 Open real /dev/null? :)
iannucci 2013/05/11 04:12:27 No bueno on windows :/
98
99
100 def train_from_tests(_base_dir_type, recipe_path):
101 if not has_test(recipe_path):
102 print 'FATAL: Recipe %s has NO tests!' % recipe_path
103 return False
104
105 for path in glob(expected_for(recipe_path, '*')):
106 os.unlink(path)
107
108 for name, test_fn in exec_test_file(recipe_path).iteritems():
109 steps = execute_test_case(test_fn, recipe_path)
110 expected_path = expected_for(recipe_path, name)
111 print 'Writing', expected_path
112 with open(expected_path, 'w') as f:
113 json.dump(steps, f, indent=2, sort_keys=True)
114
115 return True
116
117
118 def NoTestsForRecipe(recipe_path):
119 def test_DoesntExist(self):
120 self.assert_(False, 'No tests exist for %s' % recipe_path)
121 return test_DoesntExist
122
123
124 def RecipeTest(name, expected_path, test_fn, recipe_path):
125 def test_(self):
126 steps = execute_test_case(test_fn, recipe_path)
127 # Roundtrip json to get same string encoding as load
128 steps = json.loads(json.dumps(steps))
129 with open(expected_path, 'r') as f:
130 expected = json.load(f)
131 self.assertEqual(steps, expected)
132
133 test_.__name__ += name
134 return test_
135
136
137 def add_tests(_base_dir_type, recipe_path, loader, suite):
Vadim Shtayura 2013/05/11 00:33:32 For readability reasons this stuff probably can be
iannucci 2013/05/11 04:12:27 Hm... I'll take a whack at this, but I'm not sure
138 tests = []
139 if not has_test(recipe_path):
140 tests.append(NoTestsForRecipe(recipe_path))
141 else:
142 for name, test_fn in exec_test_file(recipe_path).iteritems():
143 tests.append(RecipeTest(name, expected_for(recipe_path, name),
144 test_fn, recipe_path))
145
146 suite.addTest(loader.loadTestsFromTestCase(
147 type('RecipeTest_for_%s' % os.path.basename(recipe_path),
148 (unittest.TestCase,), dict((x.__name__, x) for x in tests))))
149 return True
150
151
152 def load_tests(loader, standard_tests, _pattern):
153 assert not standard_tests.countTestCases()
154 suite = unittest.TestSuite()
155 assert not loop_over_recipes(add_tests, loader, suite)
156 return suite
157
158 def loop_over_recipes(func, *args, **kwargs):
Vadim Shtayura 2013/05/11 00:33:32 Maybe convert to a generator? 'had_errors' is used
iannucci 2013/05/11 04:12:27 Done.
159 had_errors = False
160 for name, path in BASE_DIRS.iteritems():
161 recipe_dir = os.path.join(path, 'recipes')
162 for root, _dirs, files in os.walk(recipe_dir):
163 for recipe in (f for f in files if f.endswith('.py') and f[0] != '_'):
164 recipe_path = os.path.join(root, recipe)
165 with cover():
166 # Force this file into coverage, even if there's no test for it.
167 execfile(recipe_path, {})
Vadim Shtayura 2013/05/11 00:33:32 Do we care about an error in a single recipe block
iannucci 2013/05/11 04:12:27 I think the answer is 'maybe'. I'm OK with having
168
169 if not func(name, recipe_path, *args, **kwargs):
170 had_errors = True
171 return had_errors
172
173
174 def main(argv):
175 if not os.path.exists(SLAVE_DIR):
176 os.makedirs(SLAVE_DIR)
177
178 os.chdir(SLAVE_DIR)
179
180 training = False
181 if '--help' in argv or '-h' in argv:
Vadim Shtayura 2013/05/11 00:33:32 Use ArgumentParser or OptionParser?
iannucci 2013/05/11 04:12:27 Unfortunately we can't because of the way that uni
182 print 'Pass --train to enter training mode.'
183 print
184 elif '--train' in argv:
185 argv.remove('--train')
186 training = True
187
188 had_errors = loop_over_recipes(train_from_tests) if training else False
189
190 retcode = 1 if had_errors else 0
191
192 if not training:
193 try:
194 unittest.main()
195 except SystemExit as e:
196 retcode = e.code or retcode
197
198 total_covered = COVERAGE.report()
199 if total_covered != 100.0:
200 print 'FATAL: Recipes are not at 100% coverage.'
201 retcode = retcode or 2
202
203 return retcode
204
205
206 if __name__ == '__main__':
207 sys.exit(main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698