summaryrefslogtreecommitdiff
path: root/yarn
diff options
context:
space:
mode:
Diffstat (limited to 'yarn')
-rwxr-xr-xyarn149
1 files changed, 141 insertions, 8 deletions
diff --git a/yarn b/yarn
index 842f308..f4abcdc 100755
--- a/yarn
+++ b/yarn
@@ -18,6 +18,7 @@
import cliapp
+import collections
import logging
import os
import re
@@ -63,12 +64,21 @@ class YarnRunner(cliapp.Application):
'it should be empty or not exist',
metavar='DIR')
+ self.settings.string_list(
+ ['env'],
+ 'add NAME=VALUE to the environment when tests are run',
+ metavar='NAME=VALUE')
+
self.settings.boolean(
['snapshot'],
'make snapshots of test working directory '
'after each scenario step; you probably '
'want to use this with --tempdir')
+ self.settings.boolean(
+ ['timings'],
+ 'report wall clock time for each scenario and step')
+
def info(self, msg):
if self.settings['verbose']:
logging.info(msg)
@@ -104,6 +114,9 @@ class YarnRunner(cliapp.Application):
'step %Index(step,steps): %String(step_name)')
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)
shell_prelude = self.load_shell_libraries()
@@ -111,6 +124,10 @@ class YarnRunner(cliapp.Application):
self.ts['num_scenarios'] = len(scenarios)
self.info('Found %d scenarios' % len(scenarios))
+ self.scenarios_run = 0
+ self.steps_run = 0
+ self.timings = []
+
start_time = time.time()
failed_scenarios = []
for scenario in self.select_scenarios(scenarios):
@@ -128,9 +145,13 @@ class YarnRunner(cliapp.Application):
if not self.settings['quiet']:
print (
- 'Scenario test suite PASS, with %d scenarios, '
+ 'Scenario test suite PASS, with %d scenarios '
+ '(%d total steps), '
'in %.1f seconds' %
- (len(scenarios), duration))
+ (self.scenarios_run, self.steps_run, duration))
+
+ if self.settings['timings']:
+ self.report_timings()
def parse_scenarios(self, filenames):
mdparser = yarnlib.MarkdownParser()
@@ -145,6 +166,36 @@ 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:
@@ -153,11 +204,11 @@ class YarnRunner(cliapp.Application):
def connect_implementation(self, scenario, step, implementations):
matching = [i for i in implementations
if step.what == i.what and
- re.match('(%s)$' % i.regexp, step.text, re.I)]
+ self.implements_matches_step(i, step)]
if len(matching) == 0:
raise cliapp.AppException(
- 'Scenario %s, step "%s %s" has no matching '
+ 'Scenario "%s", step "%s %s" has no matching '
'implementation' %
(scenario.name, step.what, step.text))
if len(matching) > 1:
@@ -206,17 +257,22 @@ class YarnRunner(cliapp.Application):
return scenarios
def run_scenario(self, scenario, shell_prelude):
+ 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')
+ self.remember_scenario_timing(time.time() - started)
return True
if self.settings['tempdir']:
- tempdir = self.settings['tempdir']
+ tempdir = os.path.abspath(self.settings['tempdir'])
if not os.path.exists(tempdir):
os.mkdir(tempdir)
else:
@@ -240,6 +296,9 @@ class YarnRunner(cliapp.Application):
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:
@@ -265,24 +324,65 @@ class YarnRunner(cliapp.Application):
if not self.settings['snapshot']:
shutil.rmtree(tempdir)
+ 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]
+
+ 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
+
+ return env
+
def run_step(self, datadir, scenario, step, shell_prelude, report_error):
+ started = time.time()
+
self.info('Running step "%s %s"' % (step.what, step.text))
self.ts['step'] = step
self.ts['step_name'] = '%s %s' % (step.what, step.text)
+ self.steps_run += 1
- m = re.match(step.implementation.regexp, step.text)
+ m = self.implements_matches_step(step.implementation, step)
assert m is not None
- env = os.environ.copy()
+ env = self.clean_env()
env['DATADIR'] = datadir
+ env['SRCDIR'] = os.getcwd()
for i, match in enumerate(m.groups('')):
env['MATCH_%d' % (i+1)] = match
shell_script = '%s\n\n%s\n' % (
shell_prelude, step.implementation.shell)
exit, stdout, stderr = cliapp.runcmd_unchecked(
- ['sh', '-euc', shell_script], env=env)
+ ['sh', '-xeuc', shell_script], env=env)
logging.debug('Exit code: %d' % exit)
if stdout:
@@ -303,6 +403,9 @@ class YarnRunner(cliapp.Application):
(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
def scenario_dir(self, tempdir, scenario):
@@ -336,8 +439,38 @@ 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())
+ def start_scenario_timing(self, scenario_name):
+ self.timings.append((scenario_name, None, []))
+
+ def remember_scenario_timing(self, duration):
+ scenario_name, _, step_tuples = self.timings[-1]
+ self.timings[-1] = (scenario_name, duration, step_tuples)
+
+ def remember_step_timing(self, step_name, step_duration):
+ scenario_name, scenario_duration, step_tuples = self.timings[-1]
+ step_tuples = step_tuples + [(step_name, step_duration)]
+ self.timings[-1] = (scenario_name, scenario_duration, step_tuples)
+
+ def report_timings(self):
+ for scenario_name, scenario_duration, step_tuples in self.timings:
+ print '%5.1f %s' % (scenario_duration, scenario_name)
+ for step_name, step_duration in step_tuples:
+ print ' %5.1f %s' % (step_duration, step_name)
+
YarnRunner(version=cmdtestlib.__version__).run()