diff options
author | Daniel Silverstone <dsilvers@digital-scurf.org> | 2021-01-09 16:15:35 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers@digital-scurf.org> | 2021-01-10 09:49:25 +0000 |
commit | fcdbd77cc2b909b8a1d0fc8d2e6343bbc278470c (patch) | |
tree | 05420ead169a05ae3a2b0a7c4564e230596f1fb0 /share | |
parent | e9b941e5218e5e4293bb915b13f831baba246a89 (diff) | |
download | subplot-fcdbd77cc2b909b8a1d0fc8d2e6343bbc278470c.tar.gz |
resource: Switch from 'templates' to 'share'
In a general sense, we will want to have more than just template
files as resources. This shifts from the concept that the only
thing resource-wise that subplot has is templates, to a more general
shared resources concept without a default path beyond CWD.
Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Diffstat (limited to 'share')
-rw-r--r-- | share/templates/bash/assert.sh | 19 | ||||
-rw-r--r-- | share/templates/bash/cap.sh | 17 | ||||
-rw-r--r-- | share/templates/bash/ctx.sh | 17 | ||||
-rw-r--r-- | share/templates/bash/dict.sh | 50 | ||||
-rw-r--r-- | share/templates/bash/files.sh | 22 | ||||
-rw-r--r-- | share/templates/bash/template.sh.tera | 163 | ||||
-rw-r--r-- | share/templates/bash/template.yaml | 8 | ||||
-rw-r--r-- | share/templates/python/asserts.py | 23 | ||||
-rw-r--r-- | share/templates/python/context.py | 95 | ||||
-rw-r--r-- | share/templates/python/context_tests.py | 156 | ||||
-rw-r--r-- | share/templates/python/encoding.py | 12 | ||||
-rw-r--r-- | share/templates/python/encoding_tests.py | 19 | ||||
-rw-r--r-- | share/templates/python/files.py | 23 | ||||
-rw-r--r-- | share/templates/python/main.py | 97 | ||||
-rw-r--r-- | share/templates/python/scenarios.py | 97 | ||||
-rw-r--r-- | share/templates/python/template.py.tera | 78 | ||||
-rw-r--r-- | share/templates/python/template.yaml | 9 | ||||
-rw-r--r-- | share/templates/rust/template.rs.tera | 70 | ||||
-rw-r--r-- | share/templates/rust/template.yaml | 2 |
19 files changed, 977 insertions, 0 deletions
diff --git a/share/templates/bash/assert.sh b/share/templates/bash/assert.sh new file mode 100644 index 0000000..43bb11b --- /dev/null +++ b/share/templates/bash/assert.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Check two values for equality and give error if they are not equal +assert_eq() { + if ! diff -u <(echo "$1") <(echo "$2") + then + echo "expected values to be identical, but they're not" + exit 1 + fi +} + +# Check first value contains second value. +assert_contains() { + if ! echo "$1" | grep -F "$2" > /dev/null + then + echo "expected first value to contain second value" + exit 1 + fi +} diff --git a/share/templates/bash/cap.sh b/share/templates/bash/cap.sh new file mode 100644 index 0000000..8ea35d8 --- /dev/null +++ b/share/templates/bash/cap.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Store step captures for calling the corresponding functions. + +cap_new() { + dict_new _cap +} + +cap_set() +{ + dict_set _cap "$1" "$2" +} + +cap_get() +{ + dict_get _cap "$1" +} diff --git a/share/templates/bash/ctx.sh b/share/templates/bash/ctx.sh new file mode 100644 index 0000000..c9401c6 --- /dev/null +++ b/share/templates/bash/ctx.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# A context abstraction using dictionaries. + +ctx_new() { + dict_new _ctx +} + +ctx_set() +{ + dict_set _ctx "$1" "$2" +} + +ctx_get() +{ + dict_get _ctx "$1" +} diff --git a/share/templates/bash/dict.sh b/share/templates/bash/dict.sh new file mode 100644 index 0000000..aea5b96 --- /dev/null +++ b/share/templates/bash/dict.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Simple dictionary abstraction. All values are stored in files so +# they can more easily be inspected. + +dict_new() { + local name="$1" + rm -rf "$name" + mkdir "$name" +} + +dict_has() { + local name="$1" + local key="$2" + local f="$name/$key" + test -e "$f" +} + +dict_get() { + local name="$1" + local key="$2" + local f="$name/$key" + cat "$f" +} + +dict_get_default() { + local name="$1" + local key="$2" + local default="$3" + local f="$name/$key" + if [ -e "$f" ] + then + cat "$f" + else + echo "$default" + fi +} + +dict_set() { + local name="$1" + local key="$2" + local value="$3" + local f="$name/$key" + echo "$value" > "$f" +} + +dict_keys() { + local name="$1" + ls -1 "$name" +} diff --git a/share/templates/bash/files.sh b/share/templates/bash/files.sh new file mode 100644 index 0000000..50c935d --- /dev/null +++ b/share/templates/bash/files.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Store files embedded in the markdown input. + +files_new() { + dict_new _files +} + +files_set() { + dict_set _files "$1" "$2" +} + +files_get() { + dict_get _files "$1" +} + + +# Decode a base64 encoded string. + +decode_base64() { + echo "$1" | base64 -d +} diff --git a/share/templates/bash/template.sh.tera b/share/templates/bash/template.sh.tera new file mode 100644 index 0000000..5e92371 --- /dev/null +++ b/share/templates/bash/template.sh.tera @@ -0,0 +1,163 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +############################################################################# +# Functions that implement steps. + +{% for func in functions %} +#---------------------------------------------------------------------------- +# This code comes from: {{ func.source }} + +{{ func.code }} +{% endfor %} + + +############################################################################# +# Scaffolding for generated test program. + +{% include "dict.sh" %} +{% include "ctx.sh" %} +{% include "cap.sh" %} +{% include "files.sh" %} +{% include "assert.sh" %} + +# Remember where we started from. The step functions may need to refer +# to files there. +srcdir="$(pwd)" +echo "srcdir $srcdir" + +# Create a new temporary directory and chdir there. This allows step +# functions to create new files in the current working directory +# without having to be so careful. +_datadir="$(mktemp -d)" +echo "datadir $_datadir" +cd "$_datadir" + + +# Store test data files that were embedded in the source document. +# Base64 encoding is used to allow arbitrary data. + +files_new +{% for file in files %} +# {{ file.filename }} +filename="$(decode_base64 '{{ file.filename | base64 }}')" +contents="$(decode_base64 '{{ file.contents | base64 }}')" +files_set "$filename" "$contents" +{% endfor %} + + +############################################################################# +# Code to implement the scenarios. + +{% for scenario in scenarios %} +###################################### +# Scenario: {{ scenario.title }} +scenario_{{ loop.index }}() { + local title scendir step name text ret cleanups steps + declare -a cleanups + declare -a steps + + title="$(decode_base64 '{{ scenario.title | base64 }}')" + echo "scenario: $title" + + scendir="$(mktemp -d -p "$_datadir")" + cd "$scendir" + export HOME="$scendir" + export TMPDIR="$scendir" + + ctx_new + cleanups[0]='' + steps[0]='' + ret=0 + + {% for step in scenario.steps %} + if [ "$ret" = 0 ] + then + # Step: {{ step.text }} + step="{{ step.kind | lower }} $(decode_base64 '{{ step.text | base64 }}')" + echo " step: $step" + + cap_new + {% for part in step.parts %}{% if part.CapturedText is defined -%} + name="$(decode_base64 '{{ part.CapturedText.name | base64 }}')" + text="$(decode_base64 '{{ part.CapturedText.text | base64 }}')" + cap_set "$name" "$text" + {% endif -%} + {% endfor -%} + if {{ step.function }} + then + cleanup='{{ step.cleanup }}' + if [ "$cleanup" != "" ] + then + {% raw %} + i=${#cleanups} + cleanups[$i]="$cleanup" + steps[$i]="$step" + {% endraw %} + fi + else + ret=$? + fi + fi + {% endfor %} + + {% raw %} + echo "${!cleanups[*]}" | tr ' ' '\n' | tac | while read i + do + step="${steps[$i]}" + func="${cleanups[$i]}" + echo " cleanup: $step" + $func + done + {% endraw %} + + return $ret +} +{% endfor %} + +############################################################################# +# Make the environment minimal. + +# Write to stdout the names of all environment variables, one per +# line. Handle also cases where the value of an environment variable +# contains newlines. +envnames() +{ + env -0 | xargs -0 -n1 -i'{}' sh -c "printf '%s\n' '{}' | head -n1 | sed 's/=.*//'" +} + +# Unset all environment variables. At the beginning of each scenario, +# some additional ones will be set to the per-scenario directory. +unset $(envnames) +export PATH=/bin:/usr/bin +export SHELL=/bin/sh + + +############################################################################# +# Run the scenarios. + +if [ "$#" = 0 ] +then {% for scenario in scenarios %} + scenario_{{ loop.index }}{% endfor %} +else + + + for pattern in "$@" + do + pattern="$(echo "$pattern" | tr A-Z a-z)" +{% for scenario in scenarios %} + if echo "{{ scenario.title | lower }}" | grep -F -e "$pattern" > /dev/null + then + scenario_{{ loop.index }} + fi +{% endfor %} + done +fi + + +############################################################################# +# Clean up temporary directory and report success. + +rm -rf "$_datadir" +echo "OK, all scenarios finished successfully" diff --git a/share/templates/bash/template.yaml b/share/templates/bash/template.yaml new file mode 100644 index 0000000..01269dd --- /dev/null +++ b/share/templates/bash/template.yaml @@ -0,0 +1,8 @@ +template: template.sh.tera +run: bash +helpers: + - assert.sh + - cap.sh + - ctx.sh + - dict.sh + - files.sh diff --git a/share/templates/python/asserts.py b/share/templates/python/asserts.py new file mode 100644 index 0000000..c898454 --- /dev/null +++ b/share/templates/python/asserts.py @@ -0,0 +1,23 @@ +# Check two values for equality and give error if they are not equal +def assert_eq(a, b): + assert a == b, "expected %r == %r" % (a, b) + + +# Check two values for inequality and give error if they are equal +def assert_ne(a, b): + assert a != b, "expected %r != %r" % (a, b) + + +# Check that two dict values are equal. +def assert_dict_eq(a, b): + assert isinstance(a, dict) + assert isinstance(b, dict) + for key in a: + assert key in b, f"exected {key} in both dicts" + av = a[key] + bv = b[key] + assert_eq(type(av), type(bv)) + if isinstance(av, list): + assert_eq(list(sorted(av)), list(sorted(bv))) + for key in b: + assert key in a, f"exected {key} in both dicts" diff --git a/share/templates/python/context.py b/share/templates/python/context.py new file mode 100644 index 0000000..d61316e --- /dev/null +++ b/share/templates/python/context.py @@ -0,0 +1,95 @@ +import logging +import re + + +# Store context between steps. +class Context: + def __init__(self): + self._vars = {} + self._ns = {} + + def as_dict(self): + return dict(self._vars) + + def get(self, key, default=None): + return self._vars.get(key, default) + + def __getitem__(self, key): + return self._vars[key] + + def __setitem__(self, key, value): + logging.debug("Context: key {!r} set to {!r}".format(key, value)) + self._vars[key] = value + + def __contains__(self, key): + return key in self._vars + + def __delitem__(self, key): + del self._vars[key] + + def __repr__(self): + return repr({"vars": self._vars, "namespaces": self._ns}) + + def declare(self, name): + if name not in self._ns: + self._ns[name] = NameSpace(name) + logging.debug(f"Context: declared {name}") + return self._ns[name] + + def remember_value(self, name, value): + ns = self.declare("_values") + if name in ns: + raise KeyError(name) + ns[name] = value + + def recall_value(self, name): + ns = self.declare("_values") + if name not in ns: + raise KeyError(name) + return ns[name] + + def expand_values(self, pattern): + parts = [] + while pattern: + m = re.search(r"(?<!\$)\$\{(?P<name>\S*)\}", pattern) + if not m: + parts.append(pattern) + break + name = m.group("name") + if not name: + raise KeyError("empty name in expansion") + value = self.recall_value(name) + parts.append(value) + pattern = pattern[m.end() :] + return "".join(parts) + + +class NameSpace: + def __init__(self, name): + self.name = name + self._dict = {} + + def as_dict(self): + return dict(self._dict) + + def get(self, key, default=None): + if key not in self._dict: + if default is None: + return None + self._dict[key] = default + return self._dict[key] + + def __setitem__(self, key, value): + self._dict[key] = value + + def __getitem__(self, key): + return self._dict[key] + + def __contains__(self, key): + return key in self._dict + + def __delitem__(self, key): + del self._dict[key] + + def __repr__(self): + return repr(self._dict) diff --git a/share/templates/python/context_tests.py b/share/templates/python/context_tests.py new file mode 100644 index 0000000..c91350e --- /dev/null +++ b/share/templates/python/context_tests.py @@ -0,0 +1,156 @@ +import unittest + +from context import Context + + +class ContextTests(unittest.TestCase): + def test_converts_to_empty_dict_initially(self): + ctx = Context() + self.assertEqual(ctx.as_dict(), {}) + + def test_set_item(self): + ctx = Context() + ctx["foo"] = "bar" + self.assertEqual(ctx["foo"], "bar") + + def test_does_not_contain_item(self): + ctx = Context() + self.assertFalse("foo" in ctx) + + def test_no_longer_contains_item(self): + ctx = Context() + ctx["foo"] = "bar" + del ctx["foo"] + self.assertFalse("foo" in ctx) + + def test_contains_item(self): + ctx = Context() + ctx["foo"] = "bar" + self.assertTrue("foo" in ctx) + + def test_get_returns_default_if_item_does_not_exist(self): + ctx = Context() + self.assertEqual(ctx.get("foo"), None) + + def test_get_returns_specified_default_if_item_does_not_exist(self): + ctx = Context() + self.assertEqual(ctx.get("foo", "bar"), "bar") + + def test_get_returns_value_if_item_exists(self): + ctx = Context() + ctx["foo"] = "bar" + self.assertEqual(ctx.get("foo", "yo"), "bar") + + def test_reprs_itself_when_empty(self): + ctx = Context() + self.assertFalse("foo" in repr(ctx)) + + def test_reprs_itself_when_not_empty(self): + ctx = Context() + ctx["foo"] = "bar" + self.assertTrue("foo" in repr(ctx)) + self.assertTrue("bar" in repr(ctx)) + + +class ContextMemoryTests(unittest.TestCase): + def test_recall_raises_exception_for_unremembered_value(self): + ctx = Context() + with self.assertRaises(KeyError): + ctx.recall_value("foo") + + def test_recall_returns_remembered_value(self): + ctx = Context() + ctx.remember_value("foo", "bar") + self.assertEqual(ctx.recall_value("foo"), "bar") + + def test_remember_raises_exception_for_previously_remembered(self): + ctx = Context() + ctx.remember_value("foo", "bar") + with self.assertRaises(KeyError): + ctx.remember_value("foo", "bar") + + def test_expand_returns_pattern_without_values_as_is(self): + ctx = Context() + self.assertEqual(ctx.expand_values("foo"), "foo") + + def test_expand_allows_double_dollar_escapes(self): + ctx = Context() + self.assertEqual(ctx.expand_values("$${foo}"), "$${foo}") + + def test_expand_raises_exception_for_empty_name_expansion_as_is(self): + ctx = Context() + with self.assertRaises(KeyError): + ctx.expand_values("${}") + + def test_expand_raises_error_for_unrememebered_values(self): + ctx = Context() + with self.assertRaises(KeyError): + ctx.expand_values("${foo}") + + def test_expands_rememebered_values(self): + ctx = Context() + ctx.remember_value("foo", "bar") + self.assertEqual(ctx.expand_values("${foo}"), "bar") + + +class ContextNamepaceTests(unittest.TestCase): + def test_explicit_namespaces_are_empty_dicts_initially(self): + ctx = Context() + ns = ctx.declare("foo") + self.assertEqual(ns.as_dict(), {}) + + def test_declaring_explicit_namespaces_is_idempotent(self): + ctx = Context() + ns1 = ctx.declare("foo") + ns2 = ctx.declare("foo") + self.assertEqual(id(ns1), id(ns2)) + + def test_knows_their_name(self): + ctx = Context() + ns = ctx.declare("foo") + self.assertEqual(ns.name, "foo") + + def test_sets_key(self): + ctx = Context() + ns = ctx.declare("foo") + ns["bar"] = "yo" + self.assertEqual(ns["bar"], "yo") + + def test_gets(self): + ctx = Context() + ns = ctx.declare("foo") + ns["bar"] = "yo" + self.assertEqual(ns.get("bar", "argh"), "yo") + + def test_get_without_default_doesnt_set(self): + ctx = Context() + ns = ctx.declare("foo") + ns.get("bar") + self.assertFalse("bar" in ns) + + def test_gets_with_default_sets_as_well(self): + ctx = Context() + ns = ctx.declare("foo") + self.assertEqual(ns.get("bar", "yo"), "yo") + self.assertEqual(ns["bar"], "yo") + + def test_does_not_contain_key(self): + ctx = Context() + ns = ctx.declare("foo") + self.assertFalse("bar" in ns) + + def test_contains_key(self): + ctx = Context() + ns = ctx.declare("foo") + ns["bar"] = "yo" + self.assertTrue("bar" in ns) + + def test_deletes(self): + ctx = Context() + ns = ctx.declare("foo") + ns["bar"] = "yo" + del ns["bar"] + self.assertFalse("bar" in ns) + + +unittest.main() diff --git a/share/templates/python/encoding.py b/share/templates/python/encoding.py new file mode 100644 index 0000000..1efb95e --- /dev/null +++ b/share/templates/python/encoding.py @@ -0,0 +1,12 @@ +# Decode a base64 encoded string. Result is binary or unicode string. + + +import base64 + + +def decode_bytes(s): + return base64.b64decode(s) + + +def decode_str(s): + return base64.b64decode(s).decode() diff --git a/share/templates/python/encoding_tests.py b/share/templates/python/encoding_tests.py new file mode 100644 index 0000000..4167aa4 --- /dev/null +++ b/share/templates/python/encoding_tests.py @@ -0,0 +1,19 @@ +import base64 +import unittest + +import encoding + + +class EncodingTests(unittest.TestCase): + def test_str_roundtrip(self): + original = "foo\nbar\0" + encoded = base64.b64encode(original.encode()) + self.assertEqual(encoding.decode_str(encoded), original) + + def test_bytes_roundtrip(self): + original = b"foo\nbar\0" + encoded = base64.b64encode(original) + self.assertEqual(encoding.decode_bytes(encoded), original) + + +unittest.main() diff --git a/share/templates/python/files.py b/share/templates/python/files.py new file mode 100644 index 0000000..6346172 --- /dev/null +++ b/share/templates/python/files.py @@ -0,0 +1,23 @@ +# Retrieve an embedded test data file using filename. + + +class Files: + def __init__(self): + self._files = {} + + def set(self, filename, content): + self._files[filename] = content + + def get(self, filename): + return self._files[filename] + + +_files = Files() + + +def store_file(filename, content): + _files.set(filename, content) + + +def get_file(filename): + return _files.get(filename) diff --git a/share/templates/python/main.py b/share/templates/python/main.py new file mode 100644 index 0000000..87e2782 --- /dev/null +++ b/share/templates/python/main.py @@ -0,0 +1,97 @@ +import argparse +import logging +import os +import random +import shutil +import tarfile +import tempfile + + +# Remember where we started from. The step functions may need to refer +# to files there. +srcdir = os.getcwd() +print("srcdir", srcdir) + +# Create a new temporary directory and chdir there. This allows step +# functions to create new files in the current working directory +# without having to be so careful. +_datadir = tempfile.mkdtemp() +print("datadir", _datadir) +os.chdir(_datadir) + + +def parse_command_line(): + p = argparse.ArgumentParser() + p.add_argument("--log") + p.add_argument("--env", action="append", default=[]) + p.add_argument("--save-on-failure") + p.add_argument("patterns", nargs="*") + return p.parse_args() + + +def setup_logging(args): + if args.log: + fmt = "%(asctime)s %(levelname)s %(message)s" + datefmt = "%Y-%m-%d %H:%M:%S" + formatter = logging.Formatter(fmt, datefmt) + + filename = os.path.abspath(os.path.join(srcdir, args.log)) + handler = logging.FileHandler(filename) + handler.setFormatter(formatter) + else: + handler = logging.NullHandler() + + logger = logging.getLogger() + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) + + +def save_directory(dirname, tarname): + print("tarname", tarname) + logging.info("Saving {} to {}".format(dirname, tarname)) + tar = tarfile.open(tarname, "w") + tar.add(dirname, arcname="datadir") + tar.close() + + +def main(scenarios): + args = parse_command_line() + setup_logging(args) + logging.info("Test program starts") + + logging.info("patterns: {}".format(args.patterns)) + if len(args.patterns) == 0: + logging.info("Executing all scenarios") + todo = list(scenarios) + random.shuffle(todo) + else: + logging.info("Executing requested scenarios only: {}".format(args.patterns)) + patterns = [arg.lower() for arg in args.patterns] + todo = [ + scen + for scen in scenarios + if any(pattern in scen.get_title().lower() for pattern in patterns) + ] + + extra_env = {} + for env in args.env: + (name, value) = env.split("=", 1) + extra_env[name] = value + logging.debug(f"args.env: {args.env}") + logging.debug(f"env vars from command line; {extra_env}") + + try: + for scen in todo: + scen.run(_datadir, extra_env) + except Exception as e: + logging.error(str(e), exc_info=True) + if args.save_on_failure: + print(args.save_on_failure) + filename = os.path.abspath(os.path.join(srcdir, args.save_on_failure)) + print(filename) + save_directory(_datadir, filename) + raise + + shutil.rmtree(_datadir) + print("OK, all scenarios finished successfully") + logging.info("OK, all scenarios finished successfully") diff --git a/share/templates/python/scenarios.py b/share/templates/python/scenarios.py new file mode 100644 index 0000000..e2703df --- /dev/null +++ b/share/templates/python/scenarios.py @@ -0,0 +1,97 @@ +import logging +import os +import tempfile + + +############################################################################# +# Code to implement the scenarios. + + +class Step: + def __init__(self): + self._kind = None + self._text = None + self._args = {} + self._function = None + self._cleanup = None + + def set_kind(self, kind): + self._kind = kind + + def set_text(self, text): + self._text = text + + def set_arg(self, name, value): + self._args[name] = value + + def set_function(self, function): + self._function = function + + def set_cleanup(self, cleanup): + self._cleanup = cleanup + + def do(self, ctx): + print(" step: {} {}".format(self._kind, self._text)) + logging.info(" step: {} {}".format(self._kind, self._text)) + self._function(ctx, **self._args) + + def cleanup(self, ctx): + if self._cleanup: + print(" cleanup: {} {}".format(self._kind, self._text)) + logging.info(" cleanup: {} {}".format(self._kind, self._text)) + self._cleanup(ctx) + else: + logging.info(" no cleanup defined: {} {}".format(self._kind, self._text)) + + +class Scenario: + def __init__(self, ctx): + self._title = None + self._steps = [] + self._ctx = ctx + + def get_title(self): + return self._title + + def set_title(self, title): + self._title = title + + def append_step(self, step): + self._steps.append(step) + + def run(self, datadir, extra_env): + print("scenario: {}".format(self._title)) + logging.info("Scenario: {}".format(self._title)) + logging.info("extra environment variables: {}".format(extra_env)) + + scendir = tempfile.mkdtemp(dir=datadir) + os.chdir(scendir) + self._set_environment_variables_to(scendir, extra_env) + + done = [] + ctx = self._ctx + try: + for step in self._steps: + step.do(ctx) + done.append(step) + except Exception as e: + logging.error(str(e), exc_info=True) + for step in reversed(done): + step.cleanup(ctx) + raise + for step in reversed(done): + step.cleanup(ctx) + + def _set_environment_variables_to(self, scendir, extra_env): + minimal = { + "PATH": "/bin:/usr/bin", + "SHELL": "/bin/sh", + "HOME": scendir, + "TMPDIR": scendir, + } + + os.environ.clear() + os.environ.update(minimal) + os.environ.update(extra_env) + logging.debug(f"extra_env: {dict(extra_env)!r}") + logging.debug(f"os.environ: {dict(os.environ)!r}") diff --git a/share/templates/python/template.py.tera b/share/templates/python/template.py.tera new file mode 100644 index 0000000..aa97cf0 --- /dev/null +++ b/share/templates/python/template.py.tera @@ -0,0 +1,78 @@ +############################################################################# +# Functions that implement steps. + +{% for func in functions %} +#---------------------------------------------------------------------------- +# This code comes from: {{ func.source }} + +{{ func.code }} +{% endfor %} + + +############################################################################# +# Scaffolding for generated test program. + +{% include "context.py" %} +{% include "encoding.py" %} +{% include "files.py" %} +{% include "asserts.py" %} +{% include "scenarios.py" %} +{% include "main.py" %} + + +############################################################################# +# Test data files that were embedded in the source document. Base64 +# encoding is used to allow arbitrary data. + +{% for file in files %} +# {{ file.filename }} +filename = decode_str('{{ file.filename | base64 }}') +contents = decode_bytes('{{ file.contents | base64 }}') +store_file(filename, contents) +{% endfor %} + + + +############################################################################# +# Classes for individual scenarios. + +{% for scenario in scenarios %} +#---------------------------------------------------------------------------- +# Scenario: {{ scenario.title }} +class Scenario_{{ loop.index }}(): + def __init__(self): + ctx = Context() + self._scenario = Scenario(ctx) + self._scenario.set_title(decode_str('{{ scenario.title | base64 }}')) + {% for step in scenario.steps %} + # Step: {{ step.text }} + step = Step() + step.set_kind('{{ step.kind | lower }}') + step.set_text(decode_str('{{ step.text | base64 }}')) + step.set_function({{ step.function }}) + if '{{ step.cleanup }}': + step.set_cleanup({{ step.cleanup }}) + self._scenario.append_step(step) + {% for part in step.parts %}{% if part.CapturedText is defined -%} + name = decode_str('{{ part.CapturedText.name | base64 }}') + text = decode_str('{{ part.CapturedText.text | base64 }}') + step.set_arg(name, text) + {% endif -%} + {% endfor -%} + {% endfor %} + + def get_title(self): + return self._scenario.get_title() + + def run(self, datadir, extra_env): + self._scenario.run(datadir, extra_env) +{% endfor %} + +_scenarios = { {% for scenario in scenarios %} + Scenario_{{ loop.index }}(),{% endfor %} +} + + +############################################################################# +# Call main function and clean up. +main(_scenarios) diff --git a/share/templates/python/template.yaml b/share/templates/python/template.yaml new file mode 100644 index 0000000..73f2510 --- /dev/null +++ b/share/templates/python/template.yaml @@ -0,0 +1,9 @@ +template: template.py.tera +helpers: + - context.py + - encoding.py + - files.py + - asserts.py + - scenarios.py + - main.py +run: python3 diff --git a/share/templates/rust/template.rs.tera b/share/templates/rust/template.rs.tera new file mode 100644 index 0000000..c972d37 --- /dev/null +++ b/share/templates/rust/template.rs.tera @@ -0,0 +1,70 @@ +use subplotlib::prelude::*; + +{% for func in functions %} + +// -------------------------------- +// This came from {{ func.source }} + +{{ func.code }} + +{% endfor %} + +// -------------------------------- + +lazy_static! { + static ref SUBPLOT_EMBEDDED_FILES: Vec<SubplotDataFile> = vec![ +{% for file in files %} + SubplotDataFile::new("{{ file.filename | base64 }}", + "{{ file.contents | base64 }}"), +{% endfor %} + ]; +} + +{% for scenario in scenarios %} + +// --------------------------------- + +// {{ scenario.title | commentsafe }} +#[test] +fn {{ scenario.title | nameslug }}() { + let mut scenario = Scenario::new(&base64_decode("{{scenario.title | base64}}")); + {% for step in scenario.steps %} + let step = {{step.function}}::Builder::default() + {% for part in step.parts %}{% if part.CapturedText is defined -%} + {%- set name = part.CapturedText.name -%} + {%- set text = part.CapturedText.text -%} + {%- set type = step.types[name] | default(value='text') -%} + .{{name}}( + {% if type in ['number', 'int', 'uint'] %}{{text}} + {%- elif type in ['text', 'word']%} + // "{{text | commentsafe }}" + &base64_decode("{{text | base64}}" + ) + {%- elif type in ['file'] %} + { + use std::path::PathBuf; + // {{ text | commentsafe }} + let target_name: PathBuf = base64_decode("{{ text | base64 }}").into(); + SUBPLOT_EMBEDDED_FILES + .iter() + .find(|df| df.name() == target_name) + .expect("Unable to find file at runtime") + .clone() + } + {%- else %} /* WOAH unknown type {{step.types[name]}} */ {{text}} + {%- endif %} + ) + {% endif -%} + {% endfor -%} + .build(); + {%- if step.cleanup %} + let cleanup = {{step.cleanup}}::Builder::default().build(); + scenario.add_step(step, Some(cleanup)); + {%- else %} + scenario.add_step(step, None); + {%- endif %} + {% endfor %} + + scenario.run().unwrap(); +} +{% endfor %} diff --git a/share/templates/rust/template.yaml b/share/templates/rust/template.yaml new file mode 100644 index 0000000..110f5df --- /dev/null +++ b/share/templates/rust/template.yaml @@ -0,0 +1,2 @@ +template: template.rs.tera +run: cargo test |