blob: 66dab8bc6f0605ed31723108e17e438c1d7a42f7 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import json
import os
import shutil
import subprocess
import tempfile
import unittest
import repo_test_util
from repo_test_util import ROOT_DIR
class RecipeRepo(object):
def __init__(self, recipes_path=''):
self._root = tempfile.mkdtemp()
os.makedirs(os.path.join(self._root, 'infra', 'config'))
self._recipes_cfg = os.path.join(
self._root, 'infra', 'config', 'recipes.cfg')
with open(self._recipes_cfg, 'w') as fh:
json.dump({
"api_version": 1,
"project_id": "testproj",
"recipes_path": recipes_path,
"deps": [
{
"project_id": "recipe_engine",
"url": ROOT_DIR,
"branch": "master",
"revision": "HEAD"
}
]
}, fh)
self._recipes_dir = os.path.join(self._root, 'recipes')
os.mkdir(self._recipes_dir)
self._modules_dir = os.path.join(self._root, 'recipe_modules')
os.mkdir(self._modules_dir)
def make_recipe(self, recipe, contents):
with open(os.path.join(self._recipes_dir, '%s.py' % recipe), 'w') as fh:
fh.write(contents)
def make_module(self, name, init_contents, api_contents):
module_root = os.path.join(self._modules_dir, name)
os.mkdir(module_root)
with open(os.path.join(module_root, '__init__.py'), 'w') as fh:
fh.write(init_contents)
with open(os.path.join(module_root, 'api.py'), 'w') as fh:
fh.write(api_contents)
@property
def recipes_cmd(self):
return [
os.path.join(ROOT_DIR, 'recipes.py'),
'--package', self._recipes_cfg,
'-O', 'recipe_engine=%s' % ROOT_DIR]
def __enter__(self):
return self
def __exit__(self, *_):
shutil.rmtree(self._root)
class ErrorsTest(unittest.TestCase):
def _test_cmd(self, repo, cmd, asserts=None, retcode=0, engine_args=None):
engine_args = engine_args or []
if cmd[0] == 'run':
_, path = tempfile.mkstemp('result_pb')
cmd = [cmd[0]] + ['--output-result-json', path] + cmd[1:]
if '--use-bootstrap' not in engine_args:
engine_args.insert(0, '--use-bootstrap')
try:
subp = subprocess.Popen(
repo.recipes_cmd + engine_args + cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = subp.communicate()
if asserts:
asserts(stdout, stderr)
self.assertEqual(
subp.returncode, retcode,
'%d != %d.\nstdout:\n%s\nstderr:\n%s' % (
subp.returncode, retcode, stdout, stderr))
if cmd[0] == 'run':
if not os.path.exists(path):
return
with open(path) as tf:
raw = tf.read()
data = None
if raw:
data = json.loads(raw)
return data
finally:
if cmd[0] == 'run':
if os.path.exists(path):
os.unlink(path)
def test_missing_dependency(self):
with RecipeRepo() as repo:
repo.make_recipe('foo', """
DEPS = ['aint_no_thang']
""")
subp = subprocess.Popen(
repo.recipes_cmd + ['run', 'foo'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = subp.communicate()
self.assertRegexpMatches(stdout + stderr,
r'No module named aint_no_thang', stdout + stderr)
self.assertEqual(subp.returncode, 2)
def test_missing_dependency_new(self):
with RecipeRepo() as repo:
repo.make_recipe('foo', """
DEPS = ['aint_no_thang']
""")
_, path = tempfile.mkstemp('args_pb')
with open(path, 'w') as f:
json.dump({
'engine_flags': {
'use_result_proto': True
}
}, f)
try:
def assert_nomodule(stdout, stderr):
self.assertRegexpMatches(
stdout + stderr, r'No module named aint_no_thang')
self._test_cmd(
repo, ['run', 'foo'], retcode=1, asserts=assert_nomodule,
engine_args=['--operational-args-path', path])
finally:
if os.path.exists(path):
os.unlink(path)
def test_missing_module_dependency(self):
with RecipeRepo() as repo:
repo.make_recipe('foo', 'DEPS = ["le_module"]')
repo.make_module('le_module', 'DEPS = ["love"]', '')
def assert_nomodule(stdout, stderr):
self.assertRegexpMatches(stdout + stderr, r'No module named love')
self._test_cmd(
repo, ['run', 'foo'], retcode=2, asserts=assert_nomodule)
def test_missing_module_dependency_new(self):
with RecipeRepo() as repo:
_, path = tempfile.mkstemp('args_pb')
with open(path, 'w') as f:
json.dump({
'engine_flags': {
'use_result_proto': True
}
}, f)
try:
repo.make_recipe('foo', 'DEPS = ["le_module"]')
repo.make_module('le_module', 'DEPS = ["love"]', '')
def assert_nomodule(stdout, stderr):
self.assertRegexpMatches(stdout + stderr, r'No module named love')
self._test_cmd(
repo, ['run', 'foo'], retcode=1, asserts=assert_nomodule,
engine_args=['--operational-args-path', path])
finally:
if os.path.exists(path):
os.unlink(path)
def test_no_such_recipe(self):
with RecipeRepo() as repo:
subp = subprocess.Popen(
repo.recipes_cmd + ['run', 'nooope'],
stdout=subprocess.PIPE)
stdout, _ = subp.communicate()
self.assertRegexpMatches(stdout, r'No such recipe: nooope')
self.assertEqual(subp.returncode, 2)
def test_no_such_recipe_new(self):
with RecipeRepo() as repo:
_, path = tempfile.mkstemp('args_pb')
with open(path, 'w') as f:
json.dump({
'engine_flags': {
'use_result_proto': True
}
}, f)
try:
result = self._test_cmd(
repo, ['run', 'nooope'], retcode=1,
engine_args=['--operational-args-path', path])
self.assertIsNotNone(result['failure']['exception'])
finally:
if os.path.exists(path):
os.unlink(path)
def test_syntax_error(self):
with RecipeRepo() as repo:
repo.make_recipe('foo', """
DEPS = [ (sic)
""")
def assert_syntaxerror(stdout, stderr):
self.assertRegexpMatches(stdout + stderr, r'SyntaxError')
self._test_cmd(repo, ['test', 'run', '--filter', 'foo'],
asserts=assert_syntaxerror, retcode=1)
self._test_cmd(repo, ['test', 'train', '--filter', 'foo'],
asserts=assert_syntaxerror, retcode=1)
self._test_cmd(repo, ['run', 'foo'],
asserts=assert_syntaxerror, retcode=1)
def test_missing_path(self):
with RecipeRepo() as repo:
repo.make_recipe('missing_path', """
DEPS = ['recipe_engine/step', 'recipe_engine/path']
def RunSteps(api):
api.step('do it, joe', ['echo', 'JOE'], cwd=api.path['bippityboppityboo'])
def GenTests(api):
yield api.test('basic')
""")
def assert_keyerror(stdout, stderr):
self.assertRegexpMatches(
stdout + stderr, r"KeyError: 'Unknown path: bippityboppityboo'",
stdout + stderr)
self._test_cmd(repo, ['test', 'train', '--filter', 'missing_path'],
asserts=assert_keyerror, retcode=1)
self._test_cmd(repo, ['test', 'run', '--filter', 'missing_path'],
asserts=assert_keyerror, retcode=1)
self._test_cmd(repo, ['run', 'missing_path'],
asserts=assert_keyerror, retcode=255)
def test_missing_path_new(self):
with RecipeRepo() as repo:
repo.make_recipe('missing_path', """
DEPS = ['recipe_engine/step', 'recipe_engine/path']
def RunSteps(api):
api.step('do it, joe', ['echo', 'JOE'], cwd=api.path['bippityboppityboo'])
def GenTests(api):
yield api.test('basic')
""")
def assert_keyerror(stdout, stderr):
self.assertRegexpMatches(
stdout + stderr, r"KeyError: 'Unknown path: bippityboppityboo'",
stdout + stderr)
_, path = tempfile.mkstemp('args_pb')
with open(path, 'w') as f:
json.dump({
'engine_flags': {
'use_result_proto': True
}
}, f)
try:
self._test_cmd(repo, ['test', 'train', '--filter', 'missing_path'],
asserts=assert_keyerror, retcode=1,
engine_args=['--operational-args-path', path])
self._test_cmd(repo, ['test', 'run', '--filter', 'missing_path'],
asserts=assert_keyerror, retcode=1,
engine_args=['--operational-args-path', path])
self._test_cmd(repo, ['run', 'missing_path'],
asserts=assert_keyerror, retcode=1,
engine_args=['--operational-args-path', path])
finally:
if os.path.exists(path):
os.unlink(path)
def test_engine_failure(self):
with RecipeRepo() as repo:
repo.make_recipe('print_step_error', """
DEPS = ['recipe_engine/step']
from recipe_engine import step_runner
def bad_print_step(self, step_stream, step, env):
raise Exception("Buh buh buh buh bad to the bone")
def RunSteps(api):
step_runner.SubprocessStepRunner._print_step = bad_print_step
try:
api.step('Be good', ['echo', 'Sunshine, lollipops, and rainbows'])
finally:
api.step.active_result.presentation.status = 'WARNING'
""")
self._test_cmd(repo, ['run', 'print_step_error'],
asserts=lambda stdout, stderr: self.assertRegexpMatches(
stdout + stderr,
r'(?s)Recipe engine bug.*Buh buh buh buh bad to the bone'),
retcode=2)
def test_unconsumed_assertion(self):
# There was a regression where unconsumed exceptions would not be detected
# if the exception was AssertionError.
with RecipeRepo() as repo:
repo.make_recipe('unconsumed_assertion', """
DEPS = []
def RunSteps(api):
pass
def GenTests(api):
yield api.test('basic') + api.expect_exception('AssertionError')
""")
self._test_cmd(repo, ['test', 'train', '--filter', 'unconsumed_assertion'],
asserts=lambda stdout, stderr: self.assertRegexpMatches(
stdout + stderr, 'Unconsumed'),
retcode=1)
def test_run_recipe_help(self):
with RecipeRepo(recipes_path='foo/bar') as repo:
repo.make_recipe('do_nothing', """
DEPS = []
def RunSteps(api):
pass
""")
subp = subprocess.Popen(
repo.recipes_cmd + ['run', 'do_nothing'],
stdout=subprocess.PIPE)
stdout, _ = subp.communicate()
self.assertRegexpMatches(
stdout, r'from the root of a \'testproj\' checkout')
self.assertRegexpMatches(
stdout, r'\./foo/bar/recipes\.py run .* do_nothing')
if __name__ == '__main__':
unittest.main()