From 8e8a6ee6249b365b3d50c64c442141d704e59f62 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 16 May 2020 08:33:26 +0300 Subject: feat: add optional "cleanup" function to bindings This adds an optional "cleanup" field to bindings, as the name of a function to be called when the scenario ends. As a result, the code generation templates will be able to generate code to call the cleanup functions for successful steps. The diff for this commit is a little extra long because the extra argument to various ::new functions makes lines so long, rustfmt breaks them into several lines. --- src/bindings.rs | 95 ++++++++++++++++++++++++++++++++++++++++++--------------- src/matches.rs | 4 ++- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/bindings.rs b/src/bindings.rs index 946e8fb..67c45d6 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -17,22 +17,29 @@ use regex::{escape, Regex}; /// Contains the pattern used to match against scenario steps, /// combined with the step kind. The pattern is a regular expression /// as understood by the regex crate. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Binding { kind: StepKind, pattern: String, regex: Regex, function: String, + cleanup: Option, } impl Binding { /// Create a new Binding, from a step kind and a pattern. - pub fn new(kind: StepKind, pattern: &str, function: &str) -> Result { + pub fn new( + kind: StepKind, + pattern: &str, + function: &str, + cleanup: Option<&str>, + ) -> Result { Ok(Binding { kind, pattern: pattern.to_owned(), regex: Regex::new(&format!("^{}$", pattern))?, function: function.to_string(), + cleanup: cleanup.map(String::from), }) } @@ -51,6 +58,14 @@ impl Binding { &self.function } + /// Return name of function that implements cleanup. + pub fn cleanup(&self) -> Option<&str> { + match self.cleanup { + None => None, + Some(ref s) => Some(&s), + } + } + /// Return the compiled regular expression for the pattern of the /// binding. /// @@ -69,7 +84,7 @@ impl Binding { let caps = self.regex.captures(step_text)?; // If there is only one capture, it's the whole string. - let mut m = MatchedStep::new(self.kind(), &self.function); + let mut m = MatchedStep::new(self.kind(), &self.function, self.cleanup()); if caps.len() == 1 { m.append_part(PartialStep::uncaptured(step_text)); return Some(m); @@ -125,12 +140,6 @@ impl PartialEq for Binding { impl Eq for Binding {} -impl Clone for Binding { - fn clone(&self) -> Binding { - Binding::new(self.kind, self.pattern.as_str(), &self.function).unwrap() - } -} - #[cfg(test)] mod test_binding { use super::Binding; @@ -139,47 +148,65 @@ mod test_binding { use crate::StepKind; #[test] - fn creates_new() { - let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name").unwrap(); + fn creates_new_without_cleanup() { + let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); + assert_eq!(b.kind(), StepKind::Given); + assert!(b.regex().is_match("I am Tomjon")); + assert!(!b.regex().is_match("I am Tomjon of Lancre")); + assert!(!b.regex().is_match("Hello, I am Tomjon")); + assert_eq!(b.function(), "set_name"); + assert_eq!(b.cleanup(), None); + } + + #[test] + fn creates_new_with_cleanup() { + let b = Binding::new( + StepKind::Given, + "I am Tomjon", + "set_name", + Some("unset_name"), + ) + .unwrap(); assert_eq!(b.kind(), StepKind::Given); assert!(b.regex().is_match("I am Tomjon")); assert!(!b.regex().is_match("I am Tomjon of Lancre")); assert!(!b.regex().is_match("Hello, I am Tomjon")); assert_eq!(b.function(), "set_name"); + assert_eq!(b.cleanup(), Some("unset_name")); } #[test] fn equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name").unwrap(); - let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name").unwrap(); + let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); + let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); assert_eq!(a, b); } #[test] fn not_equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name").unwrap(); - let b = Binding::new(StepKind::Given, "I am Tomjon of Lancre", "set_name").unwrap(); + let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); + let b = Binding::new(StepKind::Given, "I am Tomjon of Lancre", "set_name", None).unwrap(); assert_ne!(a, b); } #[test] fn does_not_match_with_wrong_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "yo"); - let b = Binding::new(StepKind::When, "yo", "do_yo").unwrap(); + let b = Binding::new(StepKind::When, "yo", "do_yo", None).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn does_not_match_with_wrong_text() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); - let b = Binding::new(StepKind::Given, "bar", "yo").unwrap(); + let b = Binding::new(StepKind::Given, "bar", "yo", None).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn match_with_fixed_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); - let b = Binding::new(StepKind::Given, "foo", "do_foo").unwrap(); + let b = Binding::new(StepKind::Given, "foo", "do_foo", None).unwrap(); let m = b.match_with_step(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); @@ -191,7 +218,13 @@ mod test_binding { #[test] fn match_with_regex() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon, I am"); - let b = Binding::new(StepKind::Given, r"I am (?P\S+), I am", "set_name").unwrap(); + let b = Binding::new( + StepKind::Given, + r"I am (?P\S+), I am", + "set_name", + None, + ) + .unwrap(); let m = b.match_with_step(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); @@ -220,6 +253,7 @@ struct ParsedBinding { when: Option, then: Option, function: String, + cleanup: Option, regex: Option, } @@ -331,7 +365,15 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result { regex_from_simple_pattern(pattern)? }; - Ok(Binding::new(kind, &pattern, &parsed.function)?) + Ok(Binding::new( + kind, + &pattern, + &parsed.function, + match parsed.cleanup { + None => None, + Some(ref s) => Some(s), + }, + )?) } #[cfg(test)] @@ -351,7 +393,8 @@ mod test_bindings { #[test] fn adds_binding() { - let binding = Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name").unwrap(); + let binding = + Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(&binding); assert_eq!(bindings.bindings(), &[binding]); @@ -393,7 +436,7 @@ mod test_bindings { #[test] fn does_not_find_match_for_unmatching_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::When, r"I am Tomjon", "set_foo").unwrap(); + let binding = Binding::new(StepKind::When, r"I am Tomjon", "set_foo", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(&binding); assert!(bindings.find(&step).is_none()); @@ -402,7 +445,8 @@ mod test_bindings { #[test] fn does_not_find_match_for_unmatching_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::Given, r"I am Tomjon of Lancre", "set_foo").unwrap(); + let binding = + Binding::new(StepKind::Given, r"I am Tomjon of Lancre", "set_foo", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(&binding); assert!(bindings.find(&step).is_none()); @@ -411,7 +455,7 @@ mod test_bindings { #[test] fn finds_match_for_fixed_string_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::Given, r"I am Tomjon", "set_name").unwrap(); + let binding = Binding::new(StepKind::Given, r"I am Tomjon", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(&binding); let m = bindings.find(&step).unwrap(); @@ -428,7 +472,8 @@ mod test_bindings { #[test] fn finds_match_for_regexp_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name").unwrap(); + let binding = + Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); let mut bindings = Bindings::new(); bindings.add(&binding); let m = bindings.find(&step).unwrap(); diff --git a/src/matches.rs b/src/matches.rs index 89cf791..27a71e1 100644 --- a/src/matches.rs +++ b/src/matches.rs @@ -46,16 +46,18 @@ pub struct MatchedStep { text: String, parts: Vec, function: String, + cleanup: Option, } impl MatchedStep { /// Return a new empty match. Empty means it has no step parts. - pub fn new(kind: StepKind, function: &str) -> MatchedStep { + pub fn new(kind: StepKind, function: &str, cleanup: Option<&str>) -> MatchedStep { MatchedStep { kind, text: "".to_string(), parts: vec![], function: function.to_string(), + cleanup: cleanup.map(String::from), } } -- cgit v1.2.1 From 6fe21a9836eb85fbda8cf26bdabc6b56131f79aa Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 16 May 2020 10:18:52 +0300 Subject: refactor(template.py): make Python template easier to follow It had become hard for me to follow the actual logic of the Python code running scenarios. This commit changes things so that the logic is clean Python, without any templates, using classes for steps and scenarios. The code that instantiates those classes still needs templates, but is correspondingly simpler and easier to follow. --- templates/python/template.py | 119 +++++++++++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/templates/python/template.py b/templates/python/template.py index 62b1fc9..86396ff 100644 --- a/templates/python/template.py +++ b/templates/python/template.py @@ -82,36 +82,91 @@ os.chdir(_datadir) ############################################################################# # Code to implement the scenarios. + +class Step: + + def __init__(self): + self._kind = None + self._text = None + self._args = {} + self._function = 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 do(self, ctx): + print(' step: {} {}'.format(self._kind, self._text)) + logging.info(' step: {} {}'.format(self._kind, self._text)) + self._function(ctx, **self._args) + + +class Scenario: + + def __init__(self): + self._title = None + self._steps = [] + + 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): + print('scenario: {}'.format(self._title)) + logging.info("Scenario: {}".format(self._title)) + + scendir = tempfile.mkdtemp(dir=_datadir) + os.chdir(scendir) + + ctx = Context() + for step in self._steps: + step.do(ctx) + + {% for scenario in scenarios %} ###################################### # Scenario: {{ scenario.title }} -def scenario_{{ loop.index }}(): - title = decode_str('{{ scenario.title | base64 }}') - print('scenario: {}'.format(title)) - logging.info("Scenario: {}".format(title)) - _scendir = tempfile.mkdtemp(dir=_datadir) - os.chdir(_scendir) - ctx = Context() - {% for step in scenario.steps %} - # Step: {{ step.text }} - step = decode_str('{{ step.text | base64 }}') - print(' step: {{ step.kind | lower }} {}'.format(step)) - logging.info(' step: {{ step.kind | lower }} {}'.format(step)) - args = {} - {% for part in step.parts %}{% if part.CapturedText is defined -%} - name = decode_str('{{ part.CapturedText.name | base64 }}') - text = decode_str('{{ part.CapturedText.text | base64 }}') - args[name] = text - {% endif -%} - {% endfor -%} - {{ step.function }}(ctx, **args) +class Scenario_{{ loop.index }}(): + def __init__(self): + self._scenario = Scenario() + 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 }}) + 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 %} -{% endfor %} -_scenarios = { -{% for scenario in scenarios %} - '{{ scenario.title }}': scenario_{{ loop.index }}, + def get_title(self): + return self._scenario.get_title() + + def run(self): + self._scenario.run() {% endfor %} + +_scenarios = { {% for scenario in scenarios %} + Scenario_{{ loop.index }}(),{% endfor %} } @@ -156,20 +211,20 @@ def main(): logging.info("patterns: {}".format(args.patterns)) if len(args.patterns) == 0: logging.info("Executing all scenarios") - funcs = list(_scenarios.values()) - random.shuffle(funcs) + todo = list(_scenarios) + random.shuffle(todo) else: logging.info("Executing requested scenarios only: {}".format(args.patterns)) patterns = [arg.lower() for arg in args.patterns] - funcs = [ - func - for title, func in _scenarios.items() - if any(pattern in title.lower() for pattern in patterns) + todo = [ + scen + for scen in _scenarios + if any(pattern in scen.get_title().lower() for pattern in patterns) ] try: - for func in funcs: - func() + for scen in todo: + scen.run() except Exception as e: logging.error(str(e), exc_info=True) if args.save_on_failure: -- cgit v1.2.1 From 385777c09a4f70402f10daaf79e0aa602e4e4071 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 16 May 2020 10:19:39 +0300 Subject: feat(template.py): call cleanup functions if defined If a binding has a cleanup function defined, call it at the end of the scenario, even if a step in the scenario failed. --- templates/python/template.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/templates/python/template.py b/templates/python/template.py index 86396ff..76d8239 100644 --- a/templates/python/template.py +++ b/templates/python/template.py @@ -90,6 +90,7 @@ class Step: self._text = None self._args = {} self._function = None + self._cleanup = None def set_kind(self, kind): self._kind = kind @@ -103,11 +104,22 @@ class Step: 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: @@ -132,8 +144,18 @@ class Scenario: os.chdir(scendir) ctx = Context() - for step in self._steps: - step.do(ctx) + done = [] + 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) {% for scenario in scenarios %} @@ -149,6 +171,8 @@ class Scenario_{{ loop.index }}(): 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 }}') -- cgit v1.2.1 From 0402565f12623fb56da54e9a9a764edb39129186 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 17 May 2020 11:02:45 +0300 Subject: feat(template.sh): call cleanup functions if defined --- templates/bash/template.sh | 68 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/templates/bash/template.sh b/templates/bash/template.sh index c0a6e7e..bc6a51e 100644 --- a/templates/bash/template.sh +++ b/templates/bash/template.sh @@ -167,28 +167,62 @@ files_set "$filename" "$contents" ###################################### # Scenario: {{ scenario.title }} scenario_{{ loop.index }}() { - local _title _scendir _step _name _text + local title scendir step name text ret cleanups steps + declare -a cleanups + declare -a steps - _title="$(decode_base64 '{{ scenario.title | base64 }}')" - echo "scenario: $_title" + title="$(decode_base64 '{{ scenario.title | base64 }}')" + echo "scenario: $title" - _scendir="$(mktemp -d -p "$_datadir")" - cd "$_scendir" + scendir="$(mktemp -d -p "$_datadir")" + cd "$scendir" ctx_new + cleanups[0]='' + steps[0]='' + ret=0 + {% for step in scenario.steps %} - # Step: {{ step.text }} - _step="$(decode_base64 '{{ step.text | base64 }}')" - echo " step: {{ step.kind | lower }} $_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 -%} - {{ step.function }} + 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 %} -- cgit v1.2.1 From f276c028dd4869a97129140cea28f02f3889ac47 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 16 May 2020 10:04:53 +0300 Subject: feat(subplot.md): add acceptance criteria for cleanup functionality Add acceptance checks for the cleanup functionality, for Python and Bash templates. --- subplot.md | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ subplot.py | 30 ++++++++-- subplot.yaml | 16 +++++- 3 files changed, 225 insertions(+), 6 deletions(-) diff --git a/subplot.md b/subplot.md index 05b9341..f0dfa9b 100644 --- a/subplot.md +++ b/subplot.md @@ -691,6 +691,191 @@ then bar was done ~~~~ +## Automatic cleanup in scenarios + +A binding can define a cleanup function, which gets called at the end +of the scenario in reverse order for the successful steps. If a step +fails, all the cleanups for the successful steps are still called. We +test this for every language templat we support. + +~~~{#cleanup.yaml .file .yaml .numberLines} +- given: foo + function: foo + cleanup: foo_cleanup +- given: bar + function: bar + cleanup: bar_cleanup +- given: failure + function: failure + cleanup: failure_cleanup +~~~ + +~~~{#cleanup.py .file .python .numberLines} +def foo(ctx): + pass +def foo_cleanup(ctx): + pass +def bar(ctx): + pass +def bar_cleanup(ctx): + pass +def failure(ctx): + assert 0 +def failure_cleanup(ctx): + pass +~~~ + +~~~{#cleanup.sh .file .bash .numberLines} +foo() { + true +} +foo_cleanup() { + true +} +bar() { + true +} +bar_cleanup() { + true +} +failure() { + return 1 +} +failure_cleanup() { + true +} +~~~ + + +### Cleanup functions gets called on success (Python) + +~~~scenario +given file cleanup-success-python.md +and file cleanup.yaml +and file cleanup.py +when I run sp-codegen --run cleanup-success-python.md -o test.py +then scenario "Cleanup" was run +and step "given foo" was run, and then step "given bar" +and cleanup for "given bar" was run, and then for "given foo" +and program finished successfully +~~~ + + +~~~~~{#cleanup-success-python.md .file .markdown .numberLines} +--- +title: Cleanup +bindings: cleanup.yaml +functions: cleanup.py +template: python +... + +# Cleanup + +~~~scenario +given foo +given bar +~~~ +~~~~~ + + +### Cleanup functions get called on failure (Python) + +~~~scenario +given file cleanup-fail-python.md +and file cleanup.yaml +and file cleanup.py +when I try to run sp-codegen --run cleanup-fail-python.md -o test.py +then scenario "Cleanup" was run +and step "given foo" was run, and then step "given bar" +and cleanup for "given bar" was run, and then for "given foo" +and cleanup for "given failure" was not run +and exit code is non-zero +~~~ + +~~~~~{#cleanup-fail-python.md .file .markdown .numberLines} +--- +title: Cleanup +bindings: cleanup.yaml +functions: cleanup.py +template: python +... + +# Cleanup + +~~~scenario +given foo +given bar +given failure +~~~ +~~~~~ + + +### Cleanup functions gets called on success (Bash) + +~~~scenario +given file cleanup-success-bash.md +and file cleanup.yaml +and file cleanup.sh +when I run sp-codegen --run cleanup-success-bash.md -o test.sh +then scenario "Cleanup" was run +and step "given foo" was run, and then step "given bar" +and cleanup for "given bar" was run, and then for "given foo" +and program finished successfully +~~~ + +~~~~~{#cleanup-success-bash.md .file .markdown .numberLines} +--- +title: Cleanup +bindings: cleanup.yaml +functions: cleanup.sh +template: bash +... + +# Cleanup + +~~~scenario +given foo +given bar +~~~ +~~~~~ + + +### Cleanup functions get called on failure (Bash) + +If a step fails, all the cleanups for the preceding steps are still +called, in reverse order. + +~~~scenario +given file cleanup-fail-bash.md +and file cleanup.yaml +and file cleanup.sh +when I try to run sp-codegen --run cleanup-fail-bash.md -o test.sh +then scenario "Cleanup" was run +and step "given foo" was run, and then step "given bar" +and cleanup for "given bar" was run, and then for "given foo" +and cleanup for "given failure" was not run +and exit code is non-zero +~~~ + +~~~~~{#cleanup-fail-bash.md .file .markdown .numberLines} +--- +title: Cleanup +bindings: cleanup.yaml +functions: cleanup.sh +template: bash +... + +# Cleanup + +~~~scenario +given foo +given bar +given failure +~~~ +~~~~~ + + + ## Capturing parts of steps for functions A scenario step binding can capture parts of a scenario step, to be diff --git a/subplot.py b/subplot.py index 1495553..e586b53 100644 --- a/subplot.py +++ b/subplot.py @@ -35,14 +35,14 @@ def run_docgen_with_date(ctx, md=None, output=None, date=None): exit_code_zero(ctx) -def try_codegen_and_program(ctx, filename=None): +def try_codegen_and_program(ctx, filename=None, testprog=None): codegen = binary("sp-codegen") tmpldir = os.path.join(srcdir, "templates") - runcmd(ctx, [codegen, filename, "-o", "test.py", "--run", "--templates", tmpldir]) + runcmd(ctx, [codegen, filename, "-o", testprog, "--run", "--templates", tmpldir]) -def run_codegen_and_program(ctx, filename=None): - try_codegen_and_program(ctx, filename=filename) +def run_codegen_and_program(ctx, filename=None, testprog=None): + try_codegen_and_program(ctx, filename=filename, testprog=testprog) exit_code_zero(ctx) @@ -111,6 +111,28 @@ def step_was_run(ctx, keyword=None, name=None): stdout_matches(ctx, pattern="\n step: {} {}\n".format(keyword, name)) +def step_was_run_and_then(ctx, keyword1=None, name1=None, keyword2=None, name2=None): + stdout_matches( + ctx, + pattern="\n step: {} {}\n step: {} {}".format( + keyword1, name1, keyword2, name2 + ), + ) + + +def cleanup_was_run(ctx, keyword1=None, name1=None, keyword2=None, name2=None): + stdout_matches( + ctx, + pattern="\n cleanup: {} {}\n cleanup: {} {}\n".format( + keyword1, name1, keyword2, name2 + ), + ) + + +def cleanup_was_not_run(ctx, keyword=None, name=None): + stdout_does_not_match(ctx, pattern="\n cleanup: {} {}\n".format(keyword, name)) + + def exit_code_zero(ctx): if ctx.get("exit") != 0: print("context:", ctx.as_dict()) diff --git a/subplot.yaml b/subplot.yaml index 607e361..8d4187f 100644 --- a/subplot.yaml +++ b/subplot.yaml @@ -17,13 +17,13 @@ - when: I try to run sp-docgen {md} -o {output} function: try_docgen -- when: I run sp-codegen --run {filename} -o test.py +- when: I run sp-codegen --run {filename} -o {testprog} function: run_codegen_and_program - when: I run sp-codegen {filename} -o {testprog} function: run_codegen -- when: I try to run sp-codegen --run {filename} -o test.py +- when: I try to run sp-codegen --run {filename} -o {testprog} function: try_codegen_and_program - when: I run python3 {testprog} {pattern} @@ -84,6 +84,18 @@ function: step_was_run regex: true +- then: step "(?Pgiven|when|then) (?P.+)" was run, and then step "(?Pgiven|when|then) (?P.+)" + function: step_was_run_and_then + regex: true + +- then: cleanup for "(?Pgiven|when|then) (?P.+)" was run, and then for "(?Pgiven|when|then) (?P.+)" + function: cleanup_was_run + regex: true + +- then: cleanup for "(?Pgiven|when|then) (?P.+)" was not run + function: cleanup_was_not_run + regex: true + - then: program finished successfully function: exit_code_zero -- cgit v1.2.1