summaryrefslogtreecommitdiff
path: root/yarn
diff options
context:
space:
mode:
Diffstat (limited to 'yarn')
-rwxr-xr-xyarn277
1 files changed, 87 insertions, 190 deletions
diff --git a/yarn b/yarn
index f4abcdc..6dc09c5 100755
--- a/yarn
+++ b/yarn
@@ -1,5 +1,5 @@
#!/usr/bin/python
-# Copyright 2013 Lars Wirzenius
+# Copyright 2013-2014 Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -79,6 +79,11 @@ class YarnRunner(cliapp.Application):
['timings'],
'report wall clock time for each scenario and step')
+ self.settings.boolean(
+ ['allow-missing-steps'],
+ 'allow scenarios to reference steps that do not exist, '
+ 'by warning about them, but otherwise ignoring the scenarios')
+
def info(self, msg):
if self.settings['verbose']:
logging.info(msg)
@@ -109,32 +114,50 @@ class YarnRunner(cliapp.Application):
self.ts = ttystatus.TerminalStatus(period=0.001)
if not self.settings['quiet'] and not self.settings['verbose']:
self.ts.format(
- '%ElapsedTime() %Index(scenario,scenarios): '
+ '%ElapsedTime() %Index(current_step,all_steps): '
'%String(scenario_name): '
- 'step %Index(step,steps): %String(step_name)')
+ '%String(step_name)')
+
+ if self.settings['tempdir']:
+ self.tempdir = os.path.abspath(self.settings['tempdir'])
+ if not os.path.exists(self.tempdir):
+ os.mkdir(self.tempdir)
+ else:
+ self.tempdir = tempfile.mkdtemp()
scenarios, implementations = self.parse_scenarios(args)
- self.check_there_are_scenarios(scenarios)
- self.check_for_duplicate_scenario_names(scenarios)
- self.check_for_thens(scenarios)
- self.connect_implementations(scenarios, implementations)
+ sv = yarnlib.ScenarioValidator(scenarios)
+ sv.validate_all()
+ scenarios = self.connect_implementations(scenarios, implementations)
shell_prelude = self.load_shell_libraries()
- self.ts['scenarios'] = scenarios
- self.ts['num_scenarios'] = len(scenarios)
self.info('Found %d scenarios' % len(scenarios))
+ all_steps = []
+ for scenario in scenarios:
+ all_steps.extend(scenario.steps)
+ self.ts['all_steps'] = all_steps
+
self.scenarios_run = 0
+ self.skipped_for_assuming = 0
self.steps_run = 0
self.timings = []
+ scenario_runner = yarnlib.ScenarioRunner(shell_prelude, os.getcwd(),
+ self.parse_env(),
+ pre_step_cb=self.pre_step,
+ post_step_cb=self.post_step)
+
start_time = time.time()
failed_scenarios = []
for scenario in self.select_scenarios(scenarios):
- if not self.run_scenario(scenario, shell_prelude):
+ if not self.run_scenario(scenario_runner, scenario):
failed_scenarios.append(scenario)
duration = time.time() - start_time
+ if not self.settings['snapshot']:
+ shutil.rmtree(self.tempdir)
+
if not self.settings['quiet']:
self.ts.clear()
self.ts.finish()
@@ -149,6 +172,9 @@ class YarnRunner(cliapp.Application):
'(%d total steps), '
'in %.1f seconds' %
(self.scenarios_run, self.steps_run, duration))
+ if self.skipped_for_assuming:
+ print ('Scenarios SKIPPED due to ASSUMING step failing: %d'
+ % self.skipped_for_assuming)
if self.settings['timings']:
self.report_timings()
@@ -166,76 +192,28 @@ class YarnRunner(cliapp.Application):
return block_parser.scenarios, block_parser.implementations
- def check_there_are_scenarios(self, scenarios):
- if not scenarios:
- raise cliapp.AppException(
- 'There are no scenarios; must have at least one.')
-
- def check_for_duplicate_scenario_names(self, scenarios):
- counts = collections.Counter()
- for s in scenarios:
- counts[s.name] += 1
-
- duplicates = [name for name in counts if counts[name] > 1]
- if duplicates:
- duplist = ''.join(' %s\n' % name for name in duplicates)
- raise cliapp.AppException(
- 'There are scenarios with duplicate names:\n%s' % duplist)
-
- def check_for_thens(self, scenarios):
- no_thens = []
- for scenario in scenarios:
- for step in scenario.steps:
- if step.what == 'THEN':
- break
- else:
- no_thens.append(scenario)
-
- if no_thens:
- raise cliapp.AppException(
- 'Some scenarios have no THENs:\n%s' %
- ''.join(' "%s"\n' % s.name for s in scenarios))
-
def connect_implementations(self, scenarios, implementations):
- for scenario in scenarios:
- for step in scenario.steps:
- self.connect_implementation(scenario, step, implementations)
-
- def connect_implementation(self, scenario, step, implementations):
- matching = [i for i in implementations
- if step.what == i.what and
- self.implements_matches_step(i, step)]
-
- if len(matching) == 0:
- raise cliapp.AppException(
- 'Scenario "%s", step "%s %s" has no matching '
- 'implementation' %
- (scenario.name, step.what, step.text))
- if len(matching) > 1:
- s = '\n'.join(
- 'IMPLEMENTS %s %s' % (i.what, i.regexp)
- for i in matching)
- raise cliapp.AppException(
- 'Scenario "%s", step "%s %s" has more than one '
- 'matching implementations:\n%s' %
- (scenario.name, step.what, step.text, s))
+ if self.settings['allow-missing-steps']:
+ def warn_missing(scenario, step):
+ self.warning(
+ 'Scenario %s has missing step %s %s' %
+ (scenario.name, step.what, step.text))
+ return True
+ step_connector = yarnlib.ScenarioStepConnector(
+ implementations, missing_step_cb=warn_missing)
+ else:
+ step_connector = yarnlib.ScenarioStepConnector(implementations)
- assert step.implementation is None
- step.implementation = matching[0]
+ return step_connector.connect_implementations(scenarios)
def load_shell_libraries(self):
if not self.settings['shell-library']:
self.info('No shell libraries defined')
return ''
- libs = []
- for filename in self.settings['shell-library']:
- self.info('Loading shell library %s' % filename)
- with open(filename) as f:
- text = f.read()
- libs.append('# Loaded from %s\n\n%s\n\n' % (filename, text))
-
- return ''.join(libs)
+ return yarnlib.load_shell_libraries(
+ self.settings['shell-library'],
+ pre_read_cb=lambda fn: self.info('Loading shell library %s' % fn))
def select_scenarios(self, scenarios):
@@ -256,133 +234,60 @@ class YarnRunner(cliapp.Application):
return scenarios
- def run_scenario(self, scenario, shell_prelude):
+ def run_scenario(self, scenario_runner, scenario):
self.start_scenario_timing(scenario.name)
started = time.time()
self.info('Running scenario %s' % scenario.name)
- self.ts['scenario'] = scenario
self.ts['scenario_name'] = scenario.name
- self.ts['steps'] = scenario.steps
self.scenarios_run += 1
if self.settings['no-act']:
self.info('Pretending everything went OK')
+ for step in scenario.steps:
+ self.ts['current_step'] = step
self.remember_scenario_timing(time.time() - started)
return True
- if self.settings['tempdir']:
- tempdir = os.path.abspath(self.settings['tempdir'])
- if not os.path.exists(tempdir):
- os.mkdir(tempdir)
- else:
- tempdir = tempfile.mkdtemp()
-
- os.mkdir(self.scenario_dir(tempdir, scenario))
- datadir = self.datadir(tempdir, scenario)
+ os.mkdir(self.scenario_dir(self.tempdir, scenario))
+ datadir = self.datadir(self.tempdir, scenario)
os.mkdir(datadir)
self.info('DATADIR is %s' % datadir)
+ homedir = self.homedir(datadir)
+ os.mkdir(homedir)
+ self.info('HOME for tests is %s' % homedir)
- assuming = [s for s in scenario.steps if s.what == 'ASSUMING']
- cleanup = [s for s in scenario.steps if s.what == 'FINALLY']
- normal = [s for s in scenario.steps if s not in assuming + cleanup]
-
- ok = True
- step_number = 0
-
- for step in assuming:
- exit = self.run_step(datadir, scenario, step, shell_prelude, False)
- step_number += 1
- self.snapshot_datadir(
- tempdir, datadir, scenario, step_number, step)
- if exit != 0:
- self.ts.notify(
- 'Skipping "%s" because "%s %s" failed' %
- (scenario.name, step.what, step.text))
- break
- else:
- for step in normal:
- exit = self.run_step(
- datadir, scenario, step, shell_prelude, True)
- step_number += 1
- self.snapshot_datadir(
- tempdir, datadir, scenario, step_number, step)
- if exit != 0:
- ok = False
- break
-
- for step in cleanup:
- exit = self.run_step(
- datadir, scenario, step, shell_prelude, True)
- step_number += 1
- self.snapshot_datadir(
- tempdir, datadir, scenario, step_number, step)
- if exit != 0:
- ok = False
- break
-
- if not self.settings['snapshot']:
- shutil.rmtree(tempdir)
+ ok = scenario_runner.run_scenario(scenario, datadir, homedir)
self.remember_scenario_timing(time.time() - started)
return ok
- def clean_env(self):
- '''Return a clean environment for running tests.'''
-
- whitelisted = [
- 'PATH',
- ]
-
- hardcoded = {
- 'TERM': 'dumb',
- 'SHELL': '/bin/sh',
- 'LC_ALL': 'C',
- 'USER': 'tomjon',
- 'USERNAME': 'tomjon',
- 'LOGNAME': 'tomjon',
- 'HOME': '/this/path/does/not/exist',
- }
-
- env = {}
-
- for key in whitelisted:
- if key in os.environ:
- env[key] = os.environ[key]
-
- for key in hardcoded:
- env[key] = hardcoded[key]
+ def homedir(self, datadir):
+ return os.path.join(datadir, 'HOME')
+ def parse_env(self):
for option_arg in self.settings['env']:
if '=' not in option_arg:
raise cliapp.AppException(
'--env argument must contain "=" '
'to separate environment variable name and value')
key, value = option_arg.split('=', 1)
- env[key] = value
+ yield key, value
- return env
-
- def run_step(self, datadir, scenario, step, shell_prelude, report_error):
+ def pre_step(self, step, **ignored):
started = time.time()
self.info('Running step "%s %s"' % (step.what, step.text))
- self.ts['step'] = step
+ self.ts['current_step'] = step
self.ts['step_name'] = '%s %s' % (step.what, step.text)
self.steps_run += 1
- m = self.implements_matches_step(step.implementation, step)
- assert m is not None
- env = self.clean_env()
- env['DATADIR'] = datadir
- env['SRCDIR'] = os.getcwd()
- for i, match in enumerate(m.groups('')):
- env['MATCH_%d' % (i+1)] = match
+ return (started,)
- shell_script = '%s\n\n%s\n' % (
- shell_prelude, step.implementation.shell)
- exit, stdout, stderr = cliapp.runcmd_unchecked(
- ['sh', '-xeuc', shell_script], env=env)
+ def post_step(self, scenario, step, step_number, step_env,
+ exit, stdout, stderr, pre_step_userdata):
+ stopped = time.time()
+ (started,) = pre_step_userdata
logging.debug('Exit code: %d' % exit)
if stdout:
@@ -393,20 +298,24 @@ class YarnRunner(cliapp.Application):
logging.debug('Standard error:\n%s' % self.indent(stderr))
else:
logging.debug('Standard error: empty')
-
- if exit != 0 and report_error:
- self.error(
- 'ERROR: In scenario "%s"\nstep "%s %s" failed,\n'
- 'with exit code %d:\n'
- 'Standard output from shell command:\n%s'
- 'Standard error from shell command:\n%s' %
- (scenario.name, step.what, step.text, exit,
- self.indent(stdout), self.indent(stderr)))
-
+ if exit != 0:
+ if step.what == 'ASSUMING':
+ self.ts.notify(
+ 'Skipping "%s" because "%s %s" failed' %
+ (scenario.name, step.what, step.text))
+ self.skipped_for_assuming += 1
+ else:
+ self.error(
+ 'ERROR: In scenario "%s"\nstep "%s %s" failed,\n'
+ 'with exit code %d:\n'
+ 'Standard output from shell command:\n%s'
+ 'Standard error from shell command:\n%s' %
+ (scenario.name, step.what, step.text, exit,
+ self.indent(stdout), self.indent(stderr)))
self.remember_step_timing(
- '%s %s' % (step.what, step.text), time.time() - started)
-
- return exit
+ '%s %s' % (step.what, step.text), stopped - started)
+ self.snapshot_datadir(self.tempdir, step_env['DATADIR'],
+ scenario, step_number, step)
def scenario_dir(self, tempdir, scenario):
return os.path.join(tempdir, self.nice(scenario.name))
@@ -439,18 +348,6 @@ class YarnRunner(cliapp.Application):
nice = ''.join(nice)
return nice
- def implements_matches_step(self, implements, step):
- '''Return re.Match if implements matches the step.
-
- Otherwise, return None.
-
- '''
-
- m = re.match(implements.regexp, step.text, re.I)
- if m and m.end() != len(step.text):
- return None
- return m
-
def indent(self, s):
return ''.join(' %s\n' % line for line in s.splitlines())