#!/usr/bin/python # Copyright 2013 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 # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # =*= License: GPL-3+ =*= import cliapp import logging import os import re import shutil import tempfile import time import ttystatus import cmdtestlib import yarnlib class YarnRunner(cliapp.Application): def setup(self): self.ts = ttystatus.TerminalStatus(period=0.001) self.ts.format( '%ElapsedTime() %Index(story,stories): %String(story_name): ' 'step %Index(step,steps): %String(step_name)') def process_args(self, args): stories, implementations = self.parse_stories(args) self.connect_implementations(stories, implementations) self.ts['stories'] = stories self.ts['num_stories'] = len(stories) logging.info('Found %d stories' % len(stories)) start_time = time.time() failed_stories = [] for story in stories: if not self.run_story(story): failed_stories.append(story) duration = time.time() - start_time self.ts.clear() self.ts.finish() if failed_stories: raise cliapp.AppException( 'Test suite FAILED in %s stories' % len(failed_stories)) print ('Story test suite PASS, with %d stories, in %.1f seconds' % (len(stories), duration)) def parse_stories(self, filenames): mdparser = yarnlib.MarkdownParser() for filename in filenames: mdparser.parse_file(filename) block_parser = yarnlib.BlockParser() block_parser.parse_blocks(mdparser.blocks) return block_parser.stories, block_parser.implementations def connect_implementations(self, stories, implementations): for story in stories: for step in story.steps: self.connect_implementation(story, step, implementations) def connect_implementation(self, story, step, implementations): matching = [i for i in implementations if step.what == i.what and re.match('(%s)$' % i.regexp, step.text, re.I)] if len(matching) == 0: raise cliapp.AppException( 'Story %s, step "%s %s" has no matching ' 'implementation' % (story.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( 'Story "%s", step "%s %s" has more than one ' 'matching implementations:\n%s' % (story.name, step.what, step.text, s)) assert step.implementation is None step.implementation = matching[0] def run_story(self, story): logging.info('Running story %s' % story.name) self.ts['story'] = story self.ts['story_name'] = story.name self.ts['steps'] = story.steps datadir = tempfile.mkdtemp() cleanup = [s for s in story.steps if s.what == 'FINALLY'] normal = [s for s in story.steps if s not in cleanup] ok = True for step in normal: exit = self.run_step(datadir, story, step) if exit != 0: ok = False break for step in cleanup: exit = self.run_step(datadir, story, step) if exit != 0: ok = False break shutil.rmtree(datadir) return ok def run_step(self, datadir, story, step): logging.info('Running step "%s %s"' % (step.what, step.text)) logging.info('DATADIR is %s' % datadir) self.ts['step'] = step self.ts['step_name'] = '%s %s' % (step.what, step.text) m = re.match(step.implementation.regexp, step.text) assert m is not None env = os.environ.copy() env['DATADIR'] = datadir for i, match in enumerate(m.groups('')): env['MATCH_%d' % (i+1)] = match exit, stdout, stderr = cliapp.runcmd_unchecked( ['sh', '-euc', step.implementation.shell], env=env) logging.debug('Exit code: %d' % exit) if stdout: logging.debug('Standard output:\n%s' % self.indent(stdout)) else: logging.debug('Standard output: empty') if stderr: logging.debug('Standard error:\n%s' % self.indent(stderr)) else: logging.debug('Standard error: empty') if exit != 0: self.ts.error( 'ERROR: In story "%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' % (story.name, step.what, step.text, exit, self.indent(stdout), self.indent(stderr))) return exit def indent(self, s): return ''.join(' %s\n' % line for line in s.splitlines()) YarnRunner(version=cmdtestlib.__version__).run()