summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2020-09-11 07:17:17 +0000
committerDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2020-09-11 07:17:17 +0000
commit5af231e686849eb05115cd627f935ac674321dad (patch)
tree8bbcd5b493bdf0fdd5517f43ce97758c3d4fae9b
parent04e9ea8ef3300c7c1bc3bf0a32eacead3afcebc5 (diff)
parent65995f904b605c1d57b8261c881c3e6de9608c03 (diff)
downloadsubplot-5af231e686849eb05115cd627f935ac674321dad.tar.gz
Merge branch 'inputlang' into 'main'
doc: update description of Subplot input language(s) See merge request larswirzenius/subplot!67
-rw-r--r--subplot.md189
1 files changed, 162 insertions, 27 deletions
diff --git a/subplot.md b/subplot.md
index c340763..11e3592 100644
--- a/subplot.md
+++ b/subplot.md
@@ -295,12 +295,12 @@ reference in discussions.
Subplot reads three input files, each in a different format:
-* The document file, which uses the Markdown dialect understood by
+* The document file, which uses the Markdown dialects understood by
Pandoc.
* The bindings file, in YAML.
-* The functions file, in Python.
+* The functions file, in Bash or Python.
-Subplot interprets specially marked parts of the input document
+Subplot interprets marked parts of the input document
specially. It does this via the Pandoc abstract syntax tree, rather
than text manipulation, and thus anything that Pandoc understands is
understood by Subplot. We will not specify Pandoc's dialect of
@@ -332,13 +332,24 @@ functions: subplot.py
...
~~~
+There can be more than one bindings or functions file: use a YAML
+list.
+
## Document markup
+[Pandoc]: https://pandoc.org/
+
+Subplot uses [Pandoc][], the universal document converter, to parse
+the Markdown file, and thus understands the variants of Markdown that
+Pandoc supports. This includes traditional Markdown, CommonMark, and
+GitHub-flavored Markdown.
+
[fenced code blocks]: https://pandoc.org/MANUAL.html#fenced-code-blocks
-Subplot understands certain tags for [fenced code blocks][] specially.
-A scenario, for example, would look like this:
+Subplot extends Markdown by treating certain certain tags for [fenced
+code blocks][] specially. A scenario, for example, would look like
+this:
~~~~~~{.markdown .numberLines}
```scenario
@@ -382,23 +393,29 @@ graphs. These can useful for describing things visually.
## Bindings file
-The bindings file binds scenario steps to Python functions that
+The bindings file binds scenario steps to code functions that
implement the steps. The YAML file is a list of objects (also known as
dicts or hashmaps or key/value pairs), specifying a step kind (given,
-when, then), a regular expression matching the text of the step and
+when, then), a pattern matching the text of the step and
optionally capturing interesting parts of the text, and the name of a
-function that implements the step.
+function that implements the step.
+
+Patterns can be simple or full-blown Perl-compatible regular
+expresssions ([PCRE][]).
~~~{.yaml .numberLines}
-- given: a standard setup
+- given: "a standard setup"
function: create_standard_setup
-- when: (?<thing>\S+) happens
+- when: "{thing} happens"
function: make_thing_happen
-- then: everything is OK
+- when: "I say (?P<sentence>.+) with a smile"
+ regex: true
+ function: speak
+- then: "everything is OK"
function: check_everything_is_ok
~~~
-In the example above, there are three bindings:
+In the example above, there are four bindings:
* A binding for a "given a standard setup" step. The binding captures
no part of the text, and causes the `create_standard_setup` function
@@ -407,9 +424,20 @@ In the example above, there are three bindings:
"happens". For example, "peace", as in "then peace happens". The
word is captured as "thing", and given to the `make_thing_happen`
function as an argument when it is called.
-* A binding for a "then everthing is OK" step, which captures nothing,
+* A binding for a "when" followed by "I say", an arbitrary sentence,
+ and then "with a smile", as in "when I say good morning to you with
+ a smile". The function `speak` is then called with capture named
+ "sentence" as "good morning to you".
+* A binding for a "then everything is OK" step, which captures nothing,
and calls the `check_everything_is_ok` function.
+The simple patterns (`{name}`) capture white-space delimited words. A
+pattern uses simple patterns by default, or if the `regex` field is
+set to false. To use regular expressions, `regex` must be set to true.
+Subplot complains if typical regular expression characters are used,
+when simple patterns are expected, unless `regex` is explicitly set to
+false.
+
The regular expressions use [PCRE][] syntax as implemented by the Rust
[regex][] crate. The `(?P<name>pattern)` syntax is used to capture
parts of the step. The captured parts are given to the bound function
@@ -422,23 +450,130 @@ as arguments, when it's called.
## Functions file
-The functions file is not parsed by Subplot at all. Subplot merely
-copies it to the output. All parsing and validation of the file is
-done by the Python implementation.
+Functions implementing steps are supported in Bash and Python. The
+language is chosen by setting the `template` field in the document
+YAML metadata to `bash` or `python`.
+
+The functions files are not parsed by Subplot at all. Subplot merely
+copies them to the output. All parsing and validation of the file is
+done by the programming language being used.
+
+The conventions for calling step functions vary by language. All
+languages support a "dict" abstraction of some sort. This is most
+importantly used to implement a "context" to store state in a
+controlled manner between calls to step functions. A step function can
+set a key to a value in the context, or retrieve the value for a key.
+
+Typically, a "when" step does something, and records the results into
+the context, and a "then" step checks the results by inspecting the
+context. This decouples functions from each other, and avoids having
+them use global variables for state.
+
+
+### Bash
+
+The step functions are called without any arguments.
-The Python functions must accept a "context" argument, and a keyword
-argument for each part of the step the corresponding regular
-expression captures. The capture name and the keyword argument name
-must be the same.
+The context is managed using shell functions provided by the Bash
+template:
-The context argument is a dict-like object, which the generated
-program creates automatically. The context is carried from function
-call to function call, to allow functions to manage state between
-themselves. Typically, one step might do something, and record the
-results into the context, and another step might check the results by
-inspecting the context. This decouples functions from each other, and
-avoids having them use global variables for state.
+- `ctx_set key value`
+- `ctx_get key`
+Captured values from scenario steps are passed in via another dict and
+accessed using another function:
+
+- `cap_get key`
+
+Similarly, there's a dict for embedded data files:
+
+- `files_get filename`
+
+The template provides assertion functions: `assert_eq`, `assert_contains`.
+
+Example:
+
+~~~sh
+_run()
+{
+ if "$@" < /dev/null > stdout 2> stderr
+ then
+ ctx_set exit 0
+ else
+ ctx_set exit "$?"
+ fi
+ ctx_set stdout "$(cat stdout)"
+ ctx_set stderr "$(cat stderr)"
+}
+
+run_echo_without_args()
+{
+ _run echo
+}
+
+run_echo_with_args()
+{
+ args="$(cap_get args)"
+ _run echo "$args"
+}
+
+exit_code_is()
+{
+ actual_exit="$(ctx_get exit)"
+ wanted_exit="$(cap_get exit_code)"
+ assert_eq "$actual_exit" "$wanted_exit"
+}
+
+stdout_is_a_newline()
+{
+ stdout="$(ctx_get stdout)"
+ assert_eq "$stdout" "$(printf '\n')"
+}
+
+stdout_is_text()
+{
+ stdout="$(ctx_get stdout)"
+ text="$(cap_get text)"
+ assert_contains "$stdout" "$text"
+}
+
+stderr_is_empty()
+{
+ stderr="$(ctx_get stderr)"
+ assert_eq "$stderr" ""
+}
+~~~
+
+### Python
+
+The context is implemented by a dict-like class.
+
+The step functions are called with a `ctx` argument that has the
+current state of the context, and each capture from a step as a
+keyword argument. The keyword argument name is the same as the capture
+name in the pattern in the bindings file.
+
+Embedded files are accessed using a function:
+
+- `get_file(filename)`
+
+Example:
+
+~~~python
+import json
+
+def exit_code_is(ctx, wanted=None):
+ assert_eq(ctx.get("exit"), wanted)
+
+def json_output_matches_file(ctx, filename=None):
+ actual = json.loads(ctx["stdout"])
+ expected = json.load(open(filename))
+ assert_dict_eq(actual, expected)
+
+def file_ends_in_zero_newlines(ctx, filename=None):
+ content = open(filename, "r").read()
+ assert_ne(content[-1], "\n")
+~~~
# Acceptance criteria for Subplot {#acceptance}