| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2017 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. |
| 5 |
| 6 import json |
| 7 import os |
| 8 import shutil |
| 9 import subprocess |
| 10 import sys |
| 11 import tempfile |
| 12 import unittest |
| 13 |
| 14 |
| 15 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 16 sys.path.insert(0, ROOT_DIR) |
| 17 import recipe_engine.env |
| 18 |
| 19 |
| 20 from recipe_engine import package |
| 21 from recipe_engine import package_pb2 |
| 22 |
| 23 |
| 24 class RecipeWriter(object): |
| 25 """Helper to write a recipe for tests.""" |
| 26 |
| 27 def __init__(self, recipes_dir, name): |
| 28 self.recipes_dir = recipes_dir |
| 29 self.name = name |
| 30 |
| 31 # These are expected to be set appropriately by the caller. |
| 32 self.DEPS = [] |
| 33 self.RunStepsLines = ['pass'] |
| 34 self.GenTestsLines = ['yield api.test("basic")'] |
| 35 self.expectations = {} |
| 36 |
| 37 @property |
| 38 def expect_dir(self): |
| 39 return os.path.join(self.recipes_dir, '%s.expected' % self.name) |
| 40 |
| 41 def add_expectation(self, test_name, commands=None, recipe_result=None, |
| 42 status_code=0): |
| 43 """Adds expectation for a simulation test. |
| 44 |
| 45 Arguments: |
| 46 test_name(str): name of the test |
| 47 commands(list): list of expectation dictionaries |
| 48 recipe_result(object): expected result of the recipe |
| 49 status_code(int): expected exit code |
| 50 """ |
| 51 self.expectations[test_name] = (commands or []) + [{ |
| 52 'name': '$result', |
| 53 'recipe_result': recipe_result, |
| 54 'status_code': status_code |
| 55 }] |
| 56 |
| 57 def write(self): |
| 58 """Writes the recipe to disk.""" |
| 59 for d in (self.recipes_dir, self.expect_dir): |
| 60 if not os.path.exists(d): |
| 61 os.makedirs(d) |
| 62 with open(os.path.join(self.recipes_dir, '%s.py' % self.name), 'w') as f: |
| 63 f.write('\n'.join([ |
| 64 'from recipe_engine import post_process', |
| 65 '', |
| 66 'DEPS = %r' % self.DEPS, |
| 67 '', |
| 68 'def RunSteps(api):', |
| 69 ] + [' %s' % l for l in self.RunStepsLines] + [ |
| 70 '', |
| 71 'def GenTests(api):', |
| 72 ] + [' %s' % l for l in self.GenTestsLines])) |
| 73 for test_name, test_contents in self.expectations.iteritems(): |
| 74 with open(os.path.join(self.expect_dir, '%s.json' % test_name), 'w') as f: |
| 75 json.dump(test_contents, f) |
| 76 |
| 77 |
| 78 class RecipeModuleWriter(object): |
| 79 """Helper to write a recipe module for tests.""" |
| 80 |
| 81 def __init__(self, root_dir, name): |
| 82 self.root_dir = root_dir |
| 83 self.name = name |
| 84 |
| 85 # These are expected to be set appropriately by the caller. |
| 86 self.DEPS = [] |
| 87 self.disable_strict_coverage = False |
| 88 self.methods = {} |
| 89 |
| 90 self.example = RecipeWriter(self.module_dir, 'example') |
| 91 |
| 92 @property |
| 93 def module_dir(self): |
| 94 return os.path.join(self.root_dir, 'recipe_modules', self.name) |
| 95 |
| 96 def write(self): |
| 97 """Writes the recipe module to disk.""" |
| 98 |
| 99 if not os.path.exists(self.module_dir): |
| 100 os.makedirs(self.module_dir) |
| 101 |
| 102 with open(os.path.join(self.module_dir, '__init__.py'), 'w') as f: |
| 103 f.write('DEPS = %r\n' % self.DEPS) |
| 104 if self.disable_strict_coverage: |
| 105 f.write('\nDISABLE_STRICT_COVERAGE = True') |
| 106 |
| 107 api_lines = [ |
| 108 'from recipe_engine import recipe_api', |
| 109 '', |
| 110 'class MyApi(recipe_api.RecipeApi):', |
| 111 ] |
| 112 if self.methods: |
| 113 for m_name, m_lines in self.methods.iteritems(): |
| 114 api_lines.extend([ |
| 115 '', |
| 116 ' def %s(self):' % m_name, |
| 117 ] + [' %s' % l for l in m_lines] + [ |
| 118 '', |
| 119 ]) |
| 120 else: |
| 121 api_lines.append(' pass') |
| 122 with open(os.path.join(self.module_dir, 'api.py'), 'w') as f: |
| 123 f.write('\n'.join(api_lines)) |
| 124 |
| 125 |
| 126 class TestTest(unittest.TestCase): |
| 127 def setUp(self): |
| 128 root_dir = tempfile.mkdtemp() |
| 129 config_dir = os.path.join(root_dir, 'infra', 'config') |
| 130 os.makedirs(config_dir) |
| 131 |
| 132 self._root_dir = root_dir |
| 133 self._recipes_cfg = os.path.join(config_dir, 'recipes.cfg') |
| 134 self._recipe_tool = os.path.join(ROOT_DIR, 'recipes.py') |
| 135 |
| 136 test_pkg = package_pb2.Package( |
| 137 api_version=1, |
| 138 project_id='test_pkg', |
| 139 recipes_path='', |
| 140 deps=[ |
| 141 package_pb2.DepSpec( |
| 142 project_id='recipe_engine', |
| 143 url='file://'+ROOT_DIR), |
| 144 ], |
| 145 ) |
| 146 package.ProtoFile(self._recipes_cfg).write(test_pkg) |
| 147 |
| 148 def tearDown(self): |
| 149 shutil.rmtree(self._root_dir) |
| 150 |
| 151 def _run_recipes(self, *args): |
| 152 return subprocess.check_output(( |
| 153 sys.executable, |
| 154 self._recipe_tool, |
| 155 '--use-bootstrap', |
| 156 '--package', self._recipes_cfg, |
| 157 ) + args, stderr=subprocess.STDOUT) |
| 158 |
| 159 def test_list(self): |
| 160 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 161 rw.RunStepsLines = ['pass'] |
| 162 rw.write() |
| 163 json_path = os.path.join(self._root_dir, 'tests.json') |
| 164 self._run_recipes('test', 'list', '--json', json_path) |
| 165 with open(json_path) as f: |
| 166 json_data = json.load(f) |
| 167 self.assertEqual( |
| 168 {'format': 1, 'tests': ['foo.basic']}, |
| 169 json_data) |
| 170 |
| 171 def test_test(self): |
| 172 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 173 rw.RunStepsLines = ['pass'] |
| 174 rw.add_expectation('basic') |
| 175 rw.write() |
| 176 self._run_recipes('test', 'run') |
| 177 |
| 178 def test_test_expectation_failure_empty(self): |
| 179 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 180 rw.RunStepsLines = ['pass'] |
| 181 rw.write() |
| 182 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 183 self._run_recipes('test', 'run') |
| 184 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output) |
| 185 self.assertNotIn('CHECK(FAIL)', cm.exception.output) |
| 186 self.assertIn( |
| 187 'foo.basic failed', |
| 188 cm.exception.output) |
| 189 |
| 190 def test_test_expectation_failure_different(self): |
| 191 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 192 rw.DEPS = ['recipe_engine/step'] |
| 193 rw.RunStepsLines = ['api.step("test", ["echo", "bar"])'] |
| 194 rw.add_expectation('basic') |
| 195 rw.write() |
| 196 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 197 self._run_recipes('test', 'run') |
| 198 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output) |
| 199 self.assertNotIn('CHECK(FAIL)', cm.exception.output) |
| 200 self.assertIn( |
| 201 'foo.basic failed', |
| 202 cm.exception.output) |
| 203 self.assertIn( |
| 204 '+[{\'cmd\': [\'echo\', \'bar\'], \'name\': \'test\'},\n', |
| 205 cm.exception.output) |
| 206 |
| 207 def test_test_expectation_pass(self): |
| 208 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 209 rw.DEPS = ['recipe_engine/step'] |
| 210 rw.RunStepsLines = ['api.step("test", ["echo", "bar"])'] |
| 211 rw.add_expectation('basic', [{'cmd': ['echo', 'bar'], 'name': 'test'}]) |
| 212 rw.write() |
| 213 self._run_recipes('test', 'run') |
| 214 |
| 215 def test_test_recipe_not_covered(self): |
| 216 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 217 rw.RunStepsLines = ['if False:', ' pass'] |
| 218 rw.add_expectation('basic') |
| 219 rw.write() |
| 220 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 221 self._run_recipes('test', 'run') |
| 222 self.assertIn('FATAL: Insufficient coverage', cm.exception.output) |
| 223 self.assertNotIn('CHECK(FAIL)', cm.exception.output) |
| 224 self.assertNotIn('foo.basic failed', cm.exception.output) |
| 225 |
| 226 def test_test_check_failure(self): |
| 227 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 228 rw.RunStepsLines = ['pass'] |
| 229 rw.GenTestsLines = [ |
| 230 'yield api.test("basic") + \\', |
| 231 ' api.post_process(post_process.MustRun, "bar")' |
| 232 ] |
| 233 rw.add_expectation('basic') |
| 234 rw.write() |
| 235 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 236 self._run_recipes('test', 'run') |
| 237 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output) |
| 238 self.assertIn('CHECK(FAIL)', cm.exception.output) |
| 239 self.assertIn('foo.basic failed', cm.exception.output) |
| 240 |
| 241 def test_test_check_success(self): |
| 242 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 243 rw.RunStepsLines = ['pass'] |
| 244 rw.GenTestsLines = [ |
| 245 'yield api.test("basic") + \\', |
| 246 ' api.post_process(post_process.DoesNotRun, "bar")' |
| 247 ] |
| 248 rw.add_expectation('basic') |
| 249 rw.write() |
| 250 self._run_recipes('test', 'run') |
| 251 |
| 252 def test_test_recipe_syntax_error(self): |
| 253 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo') |
| 254 rw.RunStepsLines = ['baz'] |
| 255 rw.add_expectation('basic') |
| 256 rw.write() |
| 257 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 258 self._run_recipes('test', 'run') |
| 259 self.assertIn('NameError: global name \'baz\' is not defined', |
| 260 cm.exception.output) |
| 261 |
| 262 def test_test_recipe_module_uncovered(self): |
| 263 mw = RecipeModuleWriter(self._root_dir, 'foo') |
| 264 mw.write() |
| 265 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 266 self._run_recipes('test', 'run') |
| 267 self.assertIn('The following modules lack test coverage: foo', |
| 268 cm.exception.output) |
| 269 |
| 270 def test_test_recipe_module_syntax_error(self): |
| 271 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 272 mw.methods['foo'] = ['baz'] |
| 273 mw.write() |
| 274 mw.example.DEPS = ['foo_module'] |
| 275 mw.example.RunStepsLines = ['api.foo_module.foo()'] |
| 276 mw.example.write() |
| 277 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 278 self._run_recipes('test', 'run') |
| 279 self.assertIn('NameError: global name \'baz\' is not defined', |
| 280 cm.exception.output) |
| 281 self.assertIn('FATAL: Insufficient coverage', cm.exception.output) |
| 282 |
| 283 def test_test_recipe_module_syntax_error_in_example(self): |
| 284 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 285 mw.methods['foo'] = ['pass'] |
| 286 mw.write() |
| 287 mw.example.DEPS = ['foo_module'] |
| 288 mw.example.RunStepsLines = ['baz'] |
| 289 mw.example.write() |
| 290 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 291 self._run_recipes('test', 'run') |
| 292 self.assertIn('NameError: global name \'baz\' is not defined', |
| 293 cm.exception.output) |
| 294 self.assertIn('FATAL: Insufficient coverage', cm.exception.output) |
| 295 |
| 296 def test_test_recipe_module_example_not_covered(self): |
| 297 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 298 mw.methods['foo'] = ['pass'] |
| 299 mw.write() |
| 300 mw.example.DEPS = ['foo_module'] |
| 301 mw.example.RunStepsLines = ['if False:', ' pass'] |
| 302 mw.example.write() |
| 303 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 304 self._run_recipes('test', 'run') |
| 305 self.assertIn('FATAL: Insufficient coverage', cm.exception.output) |
| 306 |
| 307 def test_test_recipe_module_uncovered_not_strict(self): |
| 308 mw = RecipeModuleWriter(self._root_dir, 'foo') |
| 309 mw.disable_strict_coverage = True |
| 310 mw.write() |
| 311 self._run_recipes('test', 'run') |
| 312 |
| 313 def test_test_recipe_module_covered_by_recipe_not_strict(self): |
| 314 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 315 mw.methods['bar'] = ['pass'] |
| 316 mw.disable_strict_coverage = True |
| 317 mw.write() |
| 318 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo_recipe') |
| 319 rw.DEPS = ['foo_module'] |
| 320 rw.RunStepsLines = ['api.foo_module.bar()'] |
| 321 rw.add_expectation('basic') |
| 322 rw.write() |
| 323 self._run_recipes('test', 'run') |
| 324 |
| 325 def test_test_recipe_module_covered_by_recipe(self): |
| 326 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 327 mw.methods['bar'] = ['pass'] |
| 328 mw.write() |
| 329 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo_recipe') |
| 330 rw.DEPS = ['foo_module'] |
| 331 rw.RunStepsLines = ['api.foo_module.bar()'] |
| 332 rw.add_expectation('basic') |
| 333 rw.write() |
| 334 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 335 self._run_recipes('test', 'run') |
| 336 self.assertIn('The following modules lack test coverage: foo_module', |
| 337 cm.exception.output) |
| 338 |
| 339 def test_test_recipe_module_partially_covered_by_recipe_not_strict(self): |
| 340 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 341 mw.methods['bar'] = ['pass'] |
| 342 mw.methods['baz'] = ['pass'] |
| 343 mw.disable_strict_coverage = True |
| 344 mw.write() |
| 345 mw.example.DEPS = ['foo_module'] |
| 346 mw.example.RunStepsLines = ['api.foo_module.baz()'] |
| 347 mw.example.add_expectation('basic') |
| 348 mw.example.write() |
| 349 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo_recipe') |
| 350 rw.DEPS = ['foo_module'] |
| 351 rw.RunStepsLines = ['api.foo_module.bar()'] |
| 352 rw.add_expectation('basic') |
| 353 rw.write() |
| 354 self._run_recipes('test', 'run') |
| 355 |
| 356 def test_test_recipe_module_partially_covered_by_recipe(self): |
| 357 mw = RecipeModuleWriter(self._root_dir, 'foo_module') |
| 358 mw.methods['bar'] = ['pass'] |
| 359 mw.methods['baz'] = ['pass'] |
| 360 mw.write() |
| 361 mw.example.DEPS = ['foo_module'] |
| 362 mw.example.RunStepsLines = ['api.foo_module.baz()'] |
| 363 mw.example.add_expectation('basic') |
| 364 mw.example.write() |
| 365 rw = RecipeWriter(os.path.join(self._root_dir, 'recipes'), 'foo_recipe') |
| 366 rw.DEPS = ['foo_module'] |
| 367 rw.RunStepsLines = ['api.foo_module.bar()'] |
| 368 rw.add_expectation('basic') |
| 369 rw.write() |
| 370 with self.assertRaises(subprocess.CalledProcessError) as cm: |
| 371 self._run_recipes('test', 'run') |
| 372 self.assertIn('FATAL: Insufficient coverage', cm.exception.output) |
| 373 |
| 374 |
| 375 if __name__ == '__main__': |
| 376 sys.exit(unittest.main()) |
| OLD | NEW |