bebraw
1/10/2010 - 12:48 PM

scenario_tester.py

from placidity.scenario_tester import Input, InputError, MatchError, \
Output, OutputError, ScenarioTester
from py.test import raises

class AbstractApplication:
    def run(self):
        while True:
            input = self.input()

            if input == 'quit':
                break

            result = self.interpret(input)

            if result:
                self.output(result)

class TestScenarioTester:
    def test_passing_test(self):
        class Application(AbstractApplication):
            def interpret(self, input):
                if input == 'a':
                    return 4

        scenario = '''
>>> a = 4
>>> a
4
>>> quit
'''

        scenario_tester = ScenarioTester(Application)
        scenario_tester.parse(scenario)
        lines = scenario_tester.lines

        assert len(lines) == 4
        self.assert_line(lines, 1, Input, 'a = 4')
        self.assert_line(lines, 2, Input, 'a')
        self.assert_line(lines, 3, Output, '4')
        self.assert_line(lines, 4, Input, 'quit')

        # this should not trigger any asserts
        scenario_tester.test(scenario)

    def test_input_fail(self):
        class Application(AbstractApplication):
            def interpret(self, input):
                pass

        scenario = '''
fail
'''

        scenario_tester = ScenarioTester(Application)
        scenario_tester.parse(scenario)
        lines = scenario_tester.lines

        assert len(lines) == 1
        self.assert_line(lines, 1, Output, 'fail')

        raises(InputError, scenario_tester.test, scenario)

    def test_match_fail(self):
        class Application(AbstractApplication):
            def interpret(self, input):
                if input == 'a':
                    return 42

        scenario = '''
>>> a = 4
>>> a
5
'''

        scenario_tester = ScenarioTester(Application)
        scenario_tester.parse(scenario)
        lines = scenario_tester.lines

        assert len(lines) == 3
        self.assert_line(lines, 1, Input, 'a = 4')
        self.assert_line(lines, 2, Input, 'a')
        self.assert_line(lines, 3, Output, '5')

        raises(MatchError, scenario_tester.test, scenario)

    def test_output_fail(self):
        class Application(AbstractApplication):
            def interpret(self, input):
                return 42

        scenario = '''
>>> fail
>>> fail
'''

        scenario_tester = ScenarioTester(Application)
        scenario_tester.parse(scenario)
        lines = scenario_tester.lines

        assert len(lines) == 2
        self.assert_line(lines, 1, Input, 'fail')
        self.assert_line(lines, 2, Input, 'fail')

        raises(OutputError, scenario_tester.test, scenario)

    def assert_line(self, lines, line_number, line_type, line_content):
        line = lines[line_number - 1]
        assert isinstance(line, line_type)
        assert line.content == line_content
from collections import deque

class InputError(Exception):
    pass

class MatchError(Exception):
    pass

class OutputError(Exception):
    pass

class Line:
    def __init__(self, content):
        self.content = content

    def __str__(self):
        return self.content

class Input(Line):
    pass

class Output(Line):
    pass

def parse_line(line):
    if len(line.strip()) == 0:
        return

    prefix = '>>> '
    if line.startswith(prefix):
        content = line.strip(prefix)
        return Input(content)
    else:
        content = line
        return Output(content)

class ScenarioTester:
    def __init__(self, app_class):
        self.app = app_class()
        self.app.input = self._input
        self.app.output = self._output

        self.lines = deque()

    def test(self, scenario):
        self.parse(scenario)
        self.app.run()

    def parse(self, scenario):
        self.lines.clear()

        for line in scenario.split('\n'):
            parsed_line = parse_line(line)

            if parsed_line:
                self.lines.append(parsed_line)

    def _input(self):
        current_line = self.lines.popleft()

        if isinstance(current_line, Input):
            return str(current_line)
        else:
            raise InputError, 'Expected input but got output instead!' + \
                ' Failed at line "%s".' % current_line

    def _output(self, result):
        current_line = self.lines.popleft()

        if isinstance(current_line, Output):
            content = current_line.content

            if content != str(result):
                raise MatchError, "Output content didn't match!" + \
                    " Expected %s (%s) but got %s (%s) instead." \
                    % (content, type(content), result, type(result))
        else:
            raise OutputError, 'Expected output but got input instead!' + \
                ' Failed at line "%s". Result: %s.' % (current_line, result)