summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2019-06-01 21:53:04 +0300
committerLars Wirzenius <liw@liw.fi>2019-06-01 21:53:04 +0300
commit8d603a5d369178a04e099acd93de450ff351022d (patch)
treef0b7d67c243e080453e6d90c892a09c06f75279e
parenta53e77f157915a0ca199e65b27d049bfa7b6ae61 (diff)
downloadfable-poc-8d603a5d369178a04e099acd93de450ff351022d.tar.gz
Add: prototype code generator, with echo example
-rw-r--r--echo-prelude.py2
-rw-r--r--echo.json299
-rw-r--r--echo.py33
-rwxr-xr-xftt-codegen154
4 files changed, 488 insertions, 0 deletions
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')