From 8d603a5d369178a04e099acd93de450ff351022d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 1 Jun 2019 21:53:04 +0300 Subject: Add: prototype code generator, with echo example --- echo-prelude.py | 2 + echo.json | 299 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ echo.py | 33 +++++++ ftt-codegen | 154 +++++++++++++++++++++++++++++ 4 files changed, 488 insertions(+) create mode 100644 echo-prelude.py create mode 100644 echo.json create mode 100644 echo.py create mode 100755 ftt-codegen diff --git a/echo-prelude.py b/echo-prelude.py new file mode 100644 index 0000000..96d45cb --- /dev/null +++ b/echo-prelude.py @@ -0,0 +1,2 @@ +from echo import * + diff --git a/echo.json b/echo.json new file mode 100644 index 0000000..c751ea7 --- /dev/null +++ b/echo.json @@ -0,0 +1,299 @@ +[ + { + "last_line_blank": false, + "start_column": 1, + "tight": false, + "end_line": 23, + "t": "Document", + "start_line": 1, + "open": false, + "children": [ + { + "last_line_blank": false, + "tight": false, + "end_line": 1, + "t": "HorizontalRule", + "start_line": 1, + "open": false, + "start_column": 1 + }, + { + "inline_content": [ + { + "tight": false, + "c": "title: \"", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "c": [ + { + "tight": false, + "c": "echo", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "open": true, + "t": "Strong", + "last_line_blank": false + }, + { + "tight": false, + "c": "(1) acceptance tests\"", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "author: Lars Wirzenius / The Fable project", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "...", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "last_line_blank": true, + "start_column": 1, + "tight": false, + "end_line": 4, + "t": "Paragraph", + "start_line": 2, + "open": false, + "strings": [ + "title: \"**echo**(1) acceptance tests\"", + "author: Lars Wirzenius / The Fable project", + "..." + ] + }, + { + "inline_content": [ + { + "tight": false, + "c": "Introduction", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "level": 1, + "last_line_blank": true, + "start_column": 1, + "tight": false, + "end_line": 7, + "t": "SetextHeader", + "start_line": 6, + "open": false, + "strings": [ + "Introduction" + ] + }, + { + "inline_content": [ + { + "tight": false, + "c": [ + { + "tight": false, + "c": "echo", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "open": true, + "t": "Strong", + "last_line_blank": false + }, + { + "tight": false, + "c": "(1) is a Unix command line tool, which writes its command line", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "arguments to the standard output. This is a simple acceptance test", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "suite for the ", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "c": "/bin/echo", + "open": true, + "t": "Code", + "last_line_blank": false + }, + { + "tight": false, + "c": " implementation.", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "last_line_blank": true, + "start_column": 1, + "tight": false, + "end_line": 11, + "t": "Paragraph", + "start_line": 9, + "open": false, + "strings": [ + "**echo**(1) is a Unix command line tool, which writes its command line", + "arguments to the standard output. This is a simple acceptance test", + "suite for the `/bin/echo` implementation." + ] + }, + { + "inline_content": [ + { + "tight": false, + "c": "No arguments", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "level": 1, + "last_line_blank": true, + "start_column": 1, + "tight": false, + "end_line": 14, + "t": "SetextHeader", + "start_line": 13, + "open": false, + "strings": [ + "No arguments" + ] + }, + { + "inline_content": [ + { + "tight": false, + "c": "This scenario runs ", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "c": "/bin/echo", + "open": true, + "t": "Code", + "last_line_blank": false + }, + { + "tight": false, + "c": " without arguments and checks that it", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "outputs a single newline character to the standard output, nothing to", + "open": true, + "t": "Str", + "last_line_blank": false + }, + { + "tight": false, + "open": true, + "t": "Softbreak", + "last_line_blank": false + }, + { + "tight": false, + "c": "the standard error, and exits with a zero exit code.", + "open": true, + "t": "Str", + "last_line_blank": false + } + ], + "last_line_blank": true, + "start_column": 1, + "tight": false, + "end_line": 18, + "t": "Paragraph", + "start_line": 16, + "open": false, + "strings": [ + "This scenario runs `/bin/echo` without arguments and checks that it", + "outputs a single newline character to the standard output, nothing to", + "the standard error, and exits with a zero exit code." + ] + }, + { + "info": "fable", + "string_content": "when user runs echo without arguments\nthen exit code is 0\nand standard output contains a newline\nand standard error is empty\n", + "fence_offset": 0, + "last_line_blank": false, + "strings": [ + "fable", + "when user runs echo without arguments", + "then exit code is 0", + "and standard output contains a newline", + "and standard error is empty" + ], + "tight": false, + "end_line": 24, + "t": "FencedCode", + "fence_char": "`", + "start_line": 20, + "open": false, + "start_column": 1, + "fence_length": 3 + } + ] + } +] diff --git a/echo.py b/echo.py new file mode 100644 index 0000000..9ec8437 --- /dev/null +++ b/echo.py @@ -0,0 +1,33 @@ +import subprocess + +context = {} + +def _save(name, value): + context[name] = value + +def _get(name): + return context[name] + +def assertEqual(a, b): + if a != b: + raise Exception( + 'expected {!r} == {!r}, but was disappointed'.format(a, b)) + +def run_echo_without_args(): + cmd = '/bin/echo' + p = subprocess.Popen( + [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + out, err = p.communicate() + context["stdout"] = out + context["stderr"] = err + context["exit_code"] = p.returncode + +def exit_code_is_zero(exit_code=None): + exit_code = int(exit_code) + assertEqual(_get("exit_code"), exit_code) + +def stdout_is_a_newline(): + assertEqual(_get('stdout'), '\n') + +def stderr_is_empty(): + assertEqual(_get('stderr'), '') diff --git a/ftt-codegen b/ftt-codegen new file mode 100755 index 0000000..7e01553 --- /dev/null +++ b/ftt-codegen @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +import copy +import re +import sys + +import CommonMark_bkrs as CommonMark +import yaml + + +class Scenario: + + def __init__(self): + self._name = None + self._steps = [] + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + def append_steps(self, steps): + self._steps.extend(steps) + + def get_steps(self): + steps = [] + prev_keyword = None + for step in self._steps: + words = step.split() + if words: + if words[0].lower() == 'and': + assert prev_keyword is not None + words[0] = prev_keyword + else: + words[0] = words[0].lower() + prev_keyword = words[0] + steps.append(' '.join(words)) + return steps + + def is_empty(self): + return len(self._steps) == 0 + + +class Fable: + + def __init__(self): + self._scenarios = [] + + def start_scenario(self, name): + s = Scenario() + s.set_name(name) + self._scenarios.append(s) + + def append_steps(self, steps): + s = self._scenarios[-1] + s.append_steps(steps) + + def get_scenarios(self): + return self._scenarios[:] + + +def collect_header(fable, o): + heading = ' '.join(o.strings) + fable.start_scenario(heading) + + +def collect_fencedcode(fable, o): + if o.info == 'fable': + fable.append_steps(o.strings[1:]) + + +collectors = { + 'ATXHeader': collect_header, + 'SetextHeader': collect_header, + 'FencedCode': collect_fencedcode, +} + + +def collect(fable, o): + if o.t not in collectors: +# debug('{} not known'.format(repr(o.t))) + return + collector = collectors[o.t] + return collector(fable, o) + + +def walk(o, func): + done = func(o) + if not done: + for c in o.children: + walk(c, func) + + +def find_binding(bindings, keyword, rest): + keyword = keyword.lower() + for b in bindings: + if keyword not in b: + continue + m = re.match(b[keyword], rest, re.I) + if not m: + continue + return b['function'], m.groupdict() + assert 0, "Couldn't find binding for {} {}".format(keyword, rest) + + +def codegen(f, step, bindings): + words = step.split() + keyword = words[0] + rest = ' '.join(words[1:]) + function, args = find_binding(bindings, keyword, rest) + f.write('args = {\n') + for arg in args: + f.write('"{}": "{}"\n'.format(arg, args[arg])) + f.write('}\n') + f.write('{}(**args)\n'.format(function)) + +def debug(msg): + if False: + sys.stderr.write('DEBUG: {}\n'.format(msg)) + sys.stderr.flush() + + +debug('reading bindings') +bindings = yaml.safe_load(open(sys.argv[1])) + +debug('reading prelude') +prelude = open(sys.argv[2]).read() +sys.stdout.write(prelude) + +debug('reading inputs') +text = ''.join(open(filename).read() for filename in sys.argv[3:]) + +debug('parse') +parser = CommonMark.DocParser() +ast = parser.parse(text) + +debug('output') +fable = Fable() +walk(ast, lambda o: collect(fable, o)) + +scenarios = [] +for s in fable.get_scenarios(): + if not s.is_empty(): + scenarios.append(s) + +for s in scenarios: + debug('scenario: {}'.format(s.get_name())) + for step in s.get_steps(): + debug(' step: {}'.format(step)) + codegen(sys.stdout, step, bindings) + sys.stdout.write('print("OK")\n') + +debug('ok') -- cgit v1.2.1