Commits: 10
Move indent_tail to tbb.py, write some tests
To run tests:
python -m unittest spec/tbb.pyindex d0041c3..0d47bfb 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ Support for Linux, BSD, OS X and Web (WASM).
= - [x] Automatic arguments conversion (casting)
= - [x] Insightful assertion errors
= - [x] Split library code from the basic interpreter
+ - [ ] Run unit tests autmatically
= - [ ] for Clojure
= - [ ] For POSIX shells
=- [ ] Capture more data in reportsindex 70d8f9b..6db7ecc 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -8,7 +8,7 @@ import unittest
=from textwrap import dedent, indent
=
=import spec.tbb as tbb
-from spec.tbb import step, log
+from spec.tbb import step, log, indent_tail
=
=
=base_command = "tbb"
@@ -62,27 +62,5 @@ def step_implementation_02(pattern: str, **kwargs):
= """)
=
=
-# TODO: Consider moving to tbb
-def indent_tail(text: str, indentation: str):
- """Adds indentation to all lines except the first one
-
- Useful when interpolating a multi-line string in a multiline f-string, like
- this:
-
- ``` python
- dedent(f'''
- Some static text
- {indent(variable_multiline_text, " ")}
- More static text
- ''')
- ```
-
- Notice that without this helper a newline in `variable_multiline_text` would
- break `dedent` by setting the indentation level to 0.
- """
-
- lines = text.splitlines()
- tail = indent(str.join("\n", lines[1:]), indentation)
- return lines[0] + tail
=
=tbb.ready()index 76d7073..1b96e52 100644
--- a/spec/tbb.py
+++ b/spec/tbb.py
@@ -8,9 +8,11 @@ import json
=import logging
=import os
=import re
+import unittest
=from textwrap import dedent
=from typing import Any, Callable
=
+
=# Setup logging
=log_level = os.environ.get("tbb_interpreter_log", "warn").upper()
=logging.basicConfig(level=getattr(logging, log_level))
@@ -163,3 +165,77 @@ def ready():
= # Most likely it indicates the end of scenario.
= # Return control to the interpreter for any cleanup.
= break
+
+
+def indent_tail(text: str, indentation: str):
+ """Adds indentation to all lines except the first one
+
+ Useful when interpolating a multi-line string in a multiline f-string, like
+ this:
+
+ ``` python
+ dedent(f'''
+ Some static text
+ {indent_tail(variable_multiline_text, " ")}
+ More static text
+ ''')
+ ```
+
+ Notice that without this helper a newline in `variable_multiline_text` would
+ break `dedent` by setting the indentation level to 0.
+ """
+
+ return re.sub("(\r?\n)", f"\\g<1>{indentation}", text)
+
+# Unit tests
+
+class IndentTailTests(unittest.TestCase):
+ def test_basic(self):
+ input = "line 1\nline 2\nline 3"
+ indentation = " "
+ expected = "line 1\n line 2\n line 3"
+ self.assertEqual(expected, indent_tail(input, indentation))
+
+ def test_one_line(self):
+ input = "line 1"
+ indentation = " "
+ expected = "line 1"
+ self.assertEqual(expected, indent_tail(input, indentation))
+
+ def test_empty(self):
+ input = ""
+ indentation = " "
+ expected = ""
+ self.assertEqual(expected, indent_tail(input, indentation))
+
+ def test_already_indented(self):
+ input = " line 1\n line 2"
+ indentation = " "
+ expected = " line 1\n line 2"
+ self.assertEqual(expected, indent_tail(input, indentation))
+
+ def test_carriege_return(self):
+ input = "line 1\r\nline 2"
+ indentation = " "
+ expected = "line 1\r\n line 2"
+ self.assertEqual(expected, indent_tail(input, indentation))
+
+ def test_in_f_string(self):
+ input = "line 1\nline 2"
+ actual = dedent(f'''
+ Some static text
+
+ {indent_tail(input, " ")}
+
+ More static text
+ ''')
+ expected = dedent(f'''
+ Some static text
+
+ line 1
+ line 2
+
+ More static text
+ ''')
+
+ self.assertEqual(expected, actual)Update the roadmap; improve regexp assert output
index 0d47bfb..19cf123 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,9 @@ Support for Linux, BSD, OS X and Web (WASM).
=- [x] Proof of concept
= - [x] Interpretter in a different language (Python)
= - [x] Report why steps fail
+- [ ] Self-check
+ - [x] Proof of concept
+ - [ ] Comprehensive
=- [ ] More readable report
= - [x] The emojis are misaligned and lack color (at least in my terminal)
= - [ ] A summary at the bottom (esp. list of errors)index 6db7ecc..68e749a 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -49,8 +49,7 @@ def step_implementation_02(pattern: str, **kwargs):
= # tester.assertRegex (output, expected_text)
=
= assert re.search(pattern, output), dedent(f"""
-
- ``` regexp
+ ``` regular-expression
= {pattern}
= ```
=Specify and implement the list subcommand
Running tbb will print the help message (to stderr) and exit with code
2. To evaluate the spec use the new tbb run command.
index d691871..6f0f0d0 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -20,3 +20,22 @@ interpreter: "python -m spec.self-check"
= * The exit code should be `0`
= * The output will contain `tad-better-behavior \d+\.\d+\.\d+`
=
+
+## Listing suites and scenarios from a single document
+
+ * Run the program with `list samples/basic.md` command line arguments
+ * The exit code should be `0`
+ * The output will contain `Basic BDD suite`
+ * The output will contain `\(python -m samples.basic\)`
+ * The output will contain `\* Arithmetic`
+ * The output will contain `\d{2}. Add 7 and 5 to get 12 \["7", "5", "12"\]`
+ * The output will contain `\d+. Divide 10 by 4 to get 2.5 \["10", "4", "2.5"\]`
+ * The output will contain `\d+. Subtract 7 from 5 to get -2 \["7", "5", "-2"\]`
+ * The output will contain `\* Text`
+ * The output will contain `\d{2}. The word blocks has 6 characters \["blocks", "6"\]`
+ * The output will contain `\d{2}. There are 3 properties in the following JSON \["3"\]`
+ * The output will contain `\d{2}. There are 3 rs in the word strawberry \["3", "r", "strawberry"\]`
+ * The output will contain `\d{2}. The following table maps words to their lengths \[\]`
+ * The output will contain `\d{2}. The reverse of abc is cba \["abc", "cba"\]`
+ * The output will contain `\d{2}. The reverse of CIA is KGB \["CIA", "KGB"\]`
+ * The output will contain `\d{2}. There are 2 os in the word boost \["2", "o", "boost"\]`index b539e3c..6c8a214 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,7 @@ mod report;
=mod spec;
=
=use anyhow::{Context, Error, bail};
-use clap::Parser;
+use clap::{Parser, Subcommand};
=use env_logger;
=use glob::glob;
=use log;
@@ -14,24 +14,79 @@ use std::path::PathBuf;
=#[derive(Parser)]
=#[command(version, about, long_about=None)]
=struct Cli {
- /// A directory or a markdown file with the spec to evaluate
- #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
- input: PathBuf,
-
= /// Enable verbose logging
= #[arg(short, long)]
= verbose: bool,
+
+ #[command(subcommand)]
+ command: Command,
=}
=
+#[derive(Subcommand)]
+enum Command {
+ List {
+ /// A directory or a markdown file with the spec to list
+ #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
+ input: PathBuf,
+ },
+
+ Run {
+ /// A directory or a markdown file with the spec to evaluate
+ #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
+ input: PathBuf,
+ },
+}
=fn main() -> Result<(), Error> {
= let cli = Cli::parse();
= let log_env = env_logger::Env::default()
= .filter_or("RUST_LOG", if cli.verbose { "trace" } else { "info" });
= env_logger::init_from_env(log_env);
=
- log::debug!("Reading the specification from {}", cli.input.display());
+ match cli.command {
+ Command::List { input } => list(input),
+ Command::Run { input } => run(input, cli.verbose),
+ }
+}
+
+fn list(input: PathBuf) -> Result<(), Error> {
+ log::debug!("Reading the specification from {}", input.display());
+
+ let input = input.canonicalize()?;
+ let mut spec = Spec::default();
+
+ if input.is_dir() {
+ log::debug!(
+ "The {} is a directory. Looking for markdown files...",
+ input.display()
+ );
+ let pattern = format!("{}/**/*.md", input.display());
+
+ for path in glob(&pattern)? {
+ let path = path.context(format!("resolving {pattern}"))?;
+ let md = std::fs::read_to_string(&path)
+ .context(format!("reading a file at {}", path.display()))?;
+ spec.load_document(&md)
+ .context(format!("loading a document from {}", path.display()))?;
+ }
+ } else if input.is_file() {
+ log::debug!("The {} is a file. Reading...", input.display());
+ let md = std::fs::read_to_string(&input)
+ .context(format!("reading a file at {}", input.display()))?;
+ spec.load_document(&md)
+ .context(format!("loading a document from {}", input.display()))?;
+ } else {
+ bail!("The {} is neither a file nor directory", input.display());
+ };
+
+ log::info!("Collected {} suites.", spec.suites.len());
+ println!("{spec}");
+ Ok(())
+}
+
+fn run(input: PathBuf, verbose: bool) -> Result<(), Error> {
+ log::debug!("Reading the specification from {}", input.display());
=
- let input = cli.input.canonicalize()?;
+ let input = input.canonicalize()?;
= let mut spec = Spec::default();
=
= if input.is_dir() {
@@ -62,7 +117,7 @@ fn main() -> Result<(), Error> {
= log::debug!("Evaluating:\n\n{spec}");
=
= let mut report = EvaluationReport::new(&spec);
- report.evaluate(cli.verbose);
+ report.evaluate(verbose);
=
= log::info!("Evaluation done. Here's the result:\n\n{report}");
=Write a spec for the run subcommand
The program doesn't conform to it yet.
Also write a prose spec for running tbb without arguments.
index 6f0f0d0..0e00456 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -39,3 +39,62 @@ interpreter: "python -m spec.self-check"
= * The output will contain `\d{2}. The reverse of abc is cba \["abc", "cba"\]`
= * The output will contain `\d{2}. The reverse of CIA is KGB \["CIA", "KGB"\]`
= * The output will contain `\d{2}. There are 2 os in the word boost \["2", "o", "boost"\]`
+
+
+## Running a spec from a single document
+
+A complete sample output is like this:
+
+``` text
+Basic BDD suite (python -m samples.basic)
+
+✓ Arithmetic
+
+ ⊞ Add 7 and 5 to get 12 ["7", "5", "12"]
+ ⊞ Divide 10 by 4 to get 2.5 ["10", "4", "2.5"]
+ ⊞ Subtract 7 from 5 to get -2 ["7", "5", "-2"]
+
+✓ Text
+
+ ⊞ The word blocks has 6 characters ["blocks", "6"]
+ ⊞ There are 3 properties in the following JSON ["3"]
+ ⊞ There are 3 rs in the word strawberry ["3", "r", "strawberry"]
+ ⊞ The following table maps words to their lengths []
+ ⊞ The reverse of abc is cba ["abc", "cba"]
+ ⊠ The reverse of CIA is KGB ["CIA", "KGB"]
+
+ 'KGB' != 'AIC'
+ - KGB
+ + AIC
+
+
+ □ There are 2 os in the word boost ["2", "o", "boost"]
+```
+
+Notice it's similar to the output of `tbb list`, but now contains unicode symbols to indicate the results of each step. Each completed scenario has a check mark `✓`. Successful steps have a squared plus `⊞` . The failing step is markd with a squared times symbol `⊠`. Once a step fails, the subsequent steps in a scenario are not exercised. In the report they are marked with a white square symbol `□`.
+
+
+ * Run the program with `run samples/basic.md` command line arguments
+ * The exit code should be `1`
+
+ The `basic.md` suit is intentionally wrong. It should be reflected in the status code.
+
+ * The output will contain `Basic BDD suite`
+ * The output will contain `\(python -m samples.basic\)`
+ * The output will contain `✓ Arithmetic`
+ * The output will contain ` ⊞ Add 7 and 5 to get 12 \["7", "5", "12"\]`
+ * The output will contain ` ⊞ Divide 10 by 4 to get 2.5 \["10", "4", "2.5"\]`
+ * The output will contain ` ⊞ Subtract 7 from 5 to get -2 \["7", "5", "-2"\]`
+ * The output will contain `✓ Text`
+ * The output will contain ` ⊞ The word blocks has 6 characters \["blocks", "6"\]`
+ * The output will contain ` ⊞ There are 3 properties in the following JSON \["3"\]`
+ * The output will contain ` ⊞ There are 3 rs in the word strawberry \["3", "r", "strawberry"\]`
+ * The output will contain ` ⊞ The following table maps words to their lengths \[\]`
+ * The output will contain ` ⊞ The reverse of abc is cba \["abc", "cba"\]`
+ * The output will contain ` The reverse of CIA is KGB \["CIA", "KGB"\]`
+ * The output will contain ` □ There are 2 os in the word boost \["2", "o", "boost"\]`
+
+
+## Running without a subcommand
+
+Running `tbb` without a subcommand will print the help message (to stderr) and exit with code 2.Signal failed evaluation with exit code
Also let the evaluation report be printed on stdout.
index 0e00456..fc96550 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -13,6 +13,7 @@ interpreter: "python -m spec.self-check"
= * The output will contain `-V, +--version +Print version`
= * The output will contain `-v, +--verbose +Enable verbose logging`
=
+TODO: Mention `run` and `list` commands.
=
=## Getting a version
=
@@ -91,10 +92,19 @@ Notice it's similar to the output of `tbb list`, but now contains unicode symbol
= * The output will contain ` ⊞ There are 3 rs in the word strawberry \["3", "r", "strawberry"\]`
= * The output will contain ` ⊞ The following table maps words to their lengths \[\]`
= * The output will contain ` ⊞ The reverse of abc is cba \["abc", "cba"\]`
- * The output will contain ` The reverse of CIA is KGB \["CIA", "KGB"\]`
= * The output will contain ` □ There are 2 os in the word boost \["2", "o", "boost"\]`
-
+ * The standard error will contain `\[.+ ERROR +tbb] Step failed: Basic BDD suite > Text > The reverse of CIA is KGB`
=
=## Running without a subcommand
=
=Running `tbb` without a subcommand will print the help message (to stderr) and exit with code 2.
+
+
+## A whole scenario failure
+
+Sometimes the failur is not in any particular step, but in a whole scenario, e.g. when an interpreter misbehaves. The status code and the summary should reflect it.
+
+
+## Multiple scenarios failure
+
+When several different scenarios fail, each one should be mentioned in the summary.index 6c8a214..37457f8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,9 +7,9 @@ use clap::{Parser, Subcommand};
=use env_logger;
=use glob::glob;
=use log;
-use report::EvaluationReport;
+use report::{EvaluationReport, EvaluationSummary};
=use spec::Spec;
-use std::path::PathBuf;
+use std::{path::PathBuf, process};
=
=#[derive(Parser)]
=#[command(version, about, long_about=None)]
@@ -119,7 +119,14 @@ fn run(input: PathBuf, verbose: bool) -> Result<(), Error> {
= let mut report = EvaluationReport::new(&spec);
= report.evaluate(verbose);
=
- log::info!("Evaluation done. Here's the result:\n\n{report}");
+ // Print the report on STDOUT
+ println!("{report}");
=
- Ok(())
+ match EvaluationSummary::from(report) {
+ EvaluationSummary::AllOk => Ok(()),
+ EvaluationSummary::Failed { failed_steps } => {
+ // TODO: Print errors from failing steps
+ process::exit(1)
+ }
+ }
=}index b3df963..178d7f3 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -347,3 +347,36 @@ pub enum InterpreterMessage {
=pub enum ControlMessage {
= Execute { step: Step },
=}
+
+pub enum EvaluationSummary {
+ AllOk,
+ Failed {
+ failed_steps: Vec<(Suite, Scenario, Step)>,
+ },
+}
+
+impl<'a> From<EvaluationReport<'a>> for EvaluationSummary {
+ fn from(report: EvaluationReport) -> Self {
+ let mut failed_steps: Vec<(Suite, Scenario, Step)> = Vec::default();
+
+ for suite in report.suites.iter() {
+ for scenario in suite.scenarios.iter() {
+ for step in scenario.steps.iter() {
+ if let StepStatus::Failed { .. } = step.status {
+ failed_steps.push((
+ suite.suite.clone(),
+ scenario.scenario.clone(),
+ step.step.clone(),
+ ));
+ }
+ }
+ }
+ }
+
+ if failed_steps.is_empty() {
+ Self::AllOk
+ } else {
+ Self::Failed { failed_steps }
+ }
+ }
+}index 5bf24d7..f0c731d 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -62,7 +62,7 @@ impl Display for Spec {
=/// From a markdown perspective, a single document can contain multiple suites,
=/// demarcated by an h1 heading. In such a case all those suites use the same
=/// interpreter, since there can be only one front-matter per document.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
=pub struct Suite {
= pub title: String,
= pub interpreter: String,
@@ -75,7 +75,7 @@ pub struct Suite {
=/// spawned and fed steps. Scenarios can be stateful, i.e. running a step can
=/// affect subsequent steps. It's up to the interpreter to implement state
=/// management.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
=pub struct Scenario {
= pub title: String,
= pub steps: Vec<Step>,Print failed steps after the report as errors
index fc96550..74fdf17 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -93,7 +93,7 @@ Notice it's similar to the output of `tbb list`, but now contains unicode symbol
= * The output will contain ` ⊞ The following table maps words to their lengths \[\]`
= * The output will contain ` ⊞ The reverse of abc is cba \["abc", "cba"\]`
= * The output will contain ` □ There are 2 os in the word boost \["2", "o", "boost"\]`
- * The standard error will contain `\[.+ ERROR +tbb] Step failed: Basic BDD suite > Text > The reverse of CIA is KGB`
+ * The standard error will contain `\[.+ ERROR +tbb\] Step failed: Basic BDD suite ❯ Text ❯ The reverse of CIA is KGB`
=
=## Running without a subcommand
=index 68e749a..71edcbb 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -60,6 +60,25 @@ def step_implementation_02(pattern: str, **kwargs):
= ```
= """)
=
+@step("The standard error will contain {0}")
+def step_implementation_03(pattern: str, **kwargs):
+ global completed
+
+ output = str(completed.stderr, "utf-8")
+
+ # The following gives unreadable output
+ # tester.assertRegex (output, expected_text)
+
+ assert re.search(pattern, output), dedent(f"""
+ ``` regular-expression
+ {pattern}
+ ```
+
+ --- not found in ---
=
+ ``` text
+ {indent_tail(output, " ")}
+ ```
+ """)
=
=tbb.ready()index 37457f8..3ac8b83 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -125,6 +125,14 @@ fn run(input: PathBuf, verbose: bool) -> Result<(), Error> {
= match EvaluationSummary::from(report) {
= EvaluationSummary::AllOk => Ok(()),
= EvaluationSummary::Failed { failed_steps } => {
+ for (suite, scenario, step) in failed_steps.iter() {
+ log::error!(
+ "Step failed: {suite} ❯ {scenario} ❯ {step}",
+ suite = suite.title,
+ scenario = scenario.title,
+ step = step.description
+ )
+ }
= // TODO: Print errors from failing steps
= process::exit(1)
= }Bump version to 0.4.0 (breaking)
To mark the introduction of run and list sub-commands (a breaking
change) and printing of the report summary.
index a4b37ac..d58a07f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.3.0"
+version = "0.4.0"
=dependencies = [
= "anyhow",
= "clap",index 32ebb5f..d7c38de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.3.0"
+version = "0.4.0"
=edition = "2024"
=
=[dependencies]index 19cf123..e3c6f0a 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ Support for Linux, BSD, OS X and Web (WASM).
= - [ ] Comprehensive
=- [ ] More readable report
= - [x] The emojis are misaligned and lack color (at least in my terminal)
- - [ ] A summary at the bottom (esp. list of errors)
+ - [x] A summary at the bottom (esp. list of errors)
= - [x] Use colors
= - [ ] More instructive error messages
= - [x] Indent multiline error messagesWrite some ideas in the readme
The big one is recursion. The important and urgent one is tags.
index e3c6f0a..e6c1b16 100644
--- a/README.md
+++ b/README.md
@@ -37,12 +37,22 @@ Support for Linux, BSD, OS X and Web (WASM).
=- [ ] Self-check
= - [x] Proof of concept
= - [ ] Comprehensive
+- [ ] Tags to filtering suites and scenarios
+ - [ ] Per document (in front-matter)
+ - [ ] Per suite (in `tags` codeblock under `h1`)
+ - [ ] Per scenario (in `tags` codeblock under `h2`)
+ - [ ] `--exclude` CLI option (logical *or*)
+ - [ ] `--only` CLI option (logical *and*)
+ - [ ] prefixes (like `suite:foo`, `scenario:ready`)
=- [ ] More readable report
= - [x] The emojis are misaligned and lack color (at least in my terminal)
= - [x] A summary at the bottom (esp. list of errors)
= - [x] Use colors
= - [ ] More instructive error messages
= - [x] Indent multiline error messages
+ - [ ] Collapse ommited steps (`□□□□□□ following 6 steps skipped`)
+ - [ ] Collapse filtered out suites
+ - [ ] Collapse filtered out scenarios
=- [ ] Pass more step data to interpreters
= - [x] Code blocks
= - [x] Tables
@@ -62,6 +72,12 @@ Support for Linux, BSD, OS X and Web (WASM).
= - [ ] Run unit tests autmatically
= - [ ] for Clojure
= - [ ] For POSIX shells
+ - [ ] For NuShell
+- [ ] Built-in interpreter (`tbb automation`)
+ - [ ] HTTP client
+ - [ ] Web Driver client
+ - [ ] E-Mail client
+ - [ ] Recursive calls (call `tbb run` and such)
=- [ ] Capture more data in reports
= - [ ] Attachments (screenshots, videos, datasets, etc.)
= - [ ] Performance data (interpreters' startup times, steps' durations)
@@ -70,3 +86,56 @@ Support for Linux, BSD, OS X and Web (WASM).
= - [ ] TUI
= - [ ] Web
=- [ ] WASM target
+
+
+# Ideas
+
+These are some ideas worth exploring. Once mature, they go on the Roadmap.
+
+### Control flow through recursion
+
+Currently there is no direct way to express "if this then that". One could implement an interpreter that would recursively run `tbb` with another suite. Something like:
+
+``` markdown
+---
+interpreter: tbb automation
+---
+
+## Plan my day
+
+``` tags
+start-here
+```
+
+ * Check current weather
+ * If `weather` is `sunny` then run scenario `Cancel all work meetings`
+
+## Cancel all work meetings
+
+ * Email the following people
+
+ - Alice <alice@example.com>
+ - Bob <bob@example.com>
+ - Carol <ceo@example.com>
+
+ ``` markdown
+ Hey!
+
+ Due to unusually nice weather I won't be available to work today. Have fun!
+
+ Sincerily,
+ Derek
+ ```
+
+ * Send message to following people
+
+ - @elen-is-fun
+ - @frank-but-fair
+
+ ``` markdown
+ See you at the beach?
+ ```
+
+```
+
+Then run itUpdate spec re commands in --help output
Introducing new step variant with a code block. I think I should convert other steps to use it too. It's much more readable.
index 74fdf17..c4f2996 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -12,6 +12,14 @@ interpreter: "python -m spec.self-check"
= * The output will contain `-h, +--help +Print help`
= * The output will contain `-V, +--version +Print version`
= * The output will contain `-v, +--verbose +Enable verbose logging`
+ * The output will contain `a sub-commands` block
+
+ ``` text
+ Commands:
+ list Print the suites, scenarios and steps of the specification
+ run Evaluate the specification
+ help Print this message or the help of the given subcommand(s)
+ ```
=
=TODO: Mention `run` and `list` commands.
=index 71edcbb..9d4ce31 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -81,4 +81,24 @@ def step_implementation_03(pattern: str, **kwargs):
= ```
= """)
=
+@step("The output will contain {0} block")
+def step_implementation_04(label: str, **kwargs):
+ global completed
+ block = kwargs['code_blocks'][0]['value']
+ output = completed.stdout.decode("utf-8")
+
+ # tester.assertIn gives unreadable output
+
+ assert block in output, dedent(f"""
+ ``` text
+ {tbb.indent_tail(block, " ")}
+ ```
+
+ --- not found in output ---
+
+ ``` text
+ {tbb.indent_tail(output, " ")}
+ ```
+ """)
+
=tbb.ready()Reorganize spec.rs - structs before traits
index f0c731d..22546c8 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -12,46 +12,6 @@ pub struct Spec {
= pub suites: Vec<Suite>,
=}
=
-impl Spec {
- /// Load suites from a markdown document
- pub fn load_document(&mut self, md: &str) -> anyhow::Result<()> {
- // TODO: Support loading multiple suits from a single document (demarcated by h1)
- let suite = Suite::from_markdown(md).context("loading a markdown document")?;
-
- self.suites.push(suite);
- Ok(())
- }
-}
-
-impl Display for Spec {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- for suite in self.suites.iter() {
- writeln!(
- f,
- "\n{title} ({interpreter})",
- title = suite.title,
- interpreter = suite.interpreter
- )?;
-
- for scenario in suite.scenarios.iter() {
- writeln!(f, "\n * {title}\n", title = scenario.title,)?;
-
- for (index, step) in scenario.steps.iter().enumerate() {
- writeln!(
- f,
- " {index:02}. {description} {arguments:?}",
- description = step.description,
- arguments = step.arguments
- )?;
- }
- }
- writeln!(f, "")?;
- }
-
- Ok(())
- }
-}
-
=/// Suite is a collection of scenarios that share a common title and interpreter
=///
=/// Other BDD systems often call it "a spec" but in my mind it doesn't make
@@ -105,6 +65,48 @@ pub struct CodeBlock {
= language: Option<String>,
= meta: Option<String>,
=}
+impl Spec {
+ /// Load suites from a markdown document
+ pub fn load_document(&mut self, md: &str) -> anyhow::Result<()> {
+ // TODO: Support loading multiple suits from a single document (demarcated by h1)
+ let suite = Suite::from_markdown(md).context("loading a markdown document")?;
+
+ self.suites.push(suite);
+ Ok(())
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Table(Vec<Vec<String>>);
+
+impl Display for Spec {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for suite in self.suites.iter() {
+ writeln!(
+ f,
+ "\n{title} ({interpreter})",
+ title = suite.title,
+ interpreter = suite.interpreter
+ )?;
+
+ for scenario in suite.scenarios.iter() {
+ writeln!(f, "\n * {title}\n", title = scenario.title,)?;
+
+ for (index, step) in scenario.steps.iter().enumerate() {
+ writeln!(
+ f,
+ " {index:02}. {description} {arguments:?}",
+ description = step.description,
+ arguments = step.arguments
+ )?;
+ }
+ }
+ writeln!(f, "")?;
+ }
+
+ Ok(())
+ }
+}
=
=impl From<&markdown::mdast::Code> for CodeBlock {
= fn from(code: &markdown::mdast::Code) -> Self {
@@ -116,9 +118,6 @@ impl From<&markdown::mdast::Code> for CodeBlock {
= }
=}
=
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct Table(Vec<Vec<String>>);
-
=impl From<&markdown::mdast::Table> for Table {
= fn from(table: &markdown::mdast::Table) -> Self {
= let rows: Vec<Vec<String>> = table