Commits: 8

Introduce an intentionally wrong step for testing

index c36e714..101d5af 100644
--- a/samples/basic.md
+++ b/samples/basic.md
@@ -25,4 +25,8 @@ Content outside of bullet points (like this) won't have any effect on the progra
=  * The word `blocks` has `6` characters
=  * There are `3` `r`s in the word `strawberry`
=  * The reverse of `abc` is `cba`
+  * The reverse of `CIA` is `KGB`
+  
+    This step is intentionally wrong to allow demonstrate that TBB will not proceed with following steps.
+    
=  * There are `2` `o`s in the word `boost`

Setup default package output from Nix flake

So that this wonder can be used in other projects.

index 98507b2..727883b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
=.devenv
=.direnv/
+result
=
=# Added by cargo
=
index b93a9d3..3807d0f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -40,5 +40,22 @@
=              ];
=            };
=          });
+
+      packages = forEachSystem
+        (system:
+          let
+            pkgs = nixpkgs.legacyPackages.${system};
+            cargo-config = builtins.fromTOML (builtins.readFile ./Cargo.toml);
+          in
+          {
+            default = pkgs.rustPlatform.buildRustPackage {
+              pname = cargo-config.package.name;
+              version = cargo-config.package.version;
+              src = ./.;
+              cargoLock = {
+                lockFile = ./Cargo.lock;
+              };
+            };
+          });
=    };
=}

Update the roadmap

index cb1c00d..aed3aea 100644
--- a/README.md
+++ b/README.md
@@ -35,16 +35,17 @@ Support for Linux, OS X and Web (WASM).
=  - [x] Interpretter in a different language (Python)
=  - [x] Report why steps fail
=- [ ] More readable report
-  - [ ] The emojis are misaligned and lack color (at least in my terminal)
+  - [x] The emojis are misaligned and lack color (at least in my terminal)
=  - [ ] A summary at the bottom (esp. list of errors)
-  - [ ] Use colors
+  - [x] Use colors
+  - [ ] More instructive error messages
=- [ ] Pass more step data to interpreters
=    - [ ] Code blocks
=    - [ ] Lists
=    - [ ] Tables
=    - [ ] Definition lists
=    - [ ] Original markdown fragment
-- [ ] Nix package (from Flake)
+- [x] Nix package (from Flake)
=- [ ] Use for evaluating Jewiet's Form to Mail specification
=- [ ] Helper libraries
=    - [ ] for Python

Use anyhow to manage errors

index 9c1b9d0..2811a83 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,6 +61,12 @@ dependencies = [
= "windows-sys 0.61.2",
=]
=
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
=[[package]]
=name = "clap"
=version = "4.5.51"
@@ -381,6 +387,7 @@ dependencies = [
=name = "tad-better-behavior"
=version = "0.1.0"
=dependencies = [
+ "anyhow",
= "clap",
= "colored",
= "env_logger",
index 3ffa046..c76fbd2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
=edition = "2024"
=
=[dependencies]
+anyhow = "1.0.100"
=clap = { version = "4.5.51", features = ["derive"] }
=colored = "3.0.0"
=env_logger = "0.11.8"
index 787b9bc..a187408 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -1,7 +1,7 @@
=use crate::spec::{Scenario, Spec, Step, Suite};
+use anyhow::anyhow;
=use colored::Colorize;
=use serde::{Deserialize, Serialize};
-use std::error::Error;
=use std::fmt::Display;
=use std::io::{BufRead, BufReader, LineWriter, Write};
=use std::process::{Command, Stdio};
@@ -58,7 +58,7 @@ pub struct ScenarioReport<'a> {
=}
=impl<'a> ScenarioReport<'a> {
=    // TODO: The ScenarioReport::run method is very effectful. I think it should live in it's own module.
-    fn run(&mut self, interpreter: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
+    fn run(&mut self, interpreter: &str, verbose: bool) -> anyhow::Result<()> {
=        log::debug!(
=            "Running scenario '{}' using '{interpreter}' as an interpreter",
=            self.scenario.title
@@ -77,11 +77,15 @@ impl<'a> ScenarioReport<'a> {
=
=        let Some(input) = process.stdin.take() else {
=            // TODO: Avoid string errors!
-            return Err("Failed to take the stdin stream of the interpreter process.".into());
+            return Err(anyhow!(
+                "Failed to take the stdin stream of the interpreter process."
+            ));
=        };
=
=        let Some(output) = process.stdout.take() else {
-            return Err("Failed to take the stdout stream of the interpreter process.".into());
+            return Err(anyhow!(
+                "Failed to take the stdout stream of the interpreter process."
+            ));
=        };
=
=        let mut reader = BufReader::new(output);
@@ -91,9 +95,9 @@ impl<'a> ScenarioReport<'a> {
=            let ready = match Self::receive(&mut reader)? {
=                InterpreterMessage::InterpreterState { ready } => ready,
=                message => {
-                    let complaint =
-                        format!("Unexpected message received from the interpreter: {message:#?}");
-                    return Err(complaint.into());
+                    return Err(anyhow!(
+                        "Unexpected message received from the interpreter: {message:#?}"
+                    ));
=                }
=            };
=            if ready {
@@ -134,10 +138,9 @@ impl<'a> ScenarioReport<'a> {
=                    break;
=                }
=                unexpected => {
-                    let message = format!(
+                    return Err(anyhow!(
=                        "Unexpected message received from the interpreter: {unexpected:#?}"
-                    );
-                    return Err(message.into());
+                    ));
=                }
=            };
=        }
@@ -146,15 +149,14 @@ impl<'a> ScenarioReport<'a> {
=
=        process
=            .wait()
-            .map_err(|io_error| Box::new(io_error).into())  // No idea why it's needed.
+            .map_err(|io_error| { anyhow::Error::from(io_error) })
=            .and_then(|exit_status| {
=                log::debug!("Interpreter closed with exit status {exit_status:?}");
=                let exit_code = exit_status.code().unwrap_or(0); // Is that right?
=                if exit_code == 0 {
=                    Ok(())
=                } else {
-                    let message = format!("Interpreter process {interpreter} exited abnormally. Exit code: {exit_code}");
-                    Err(message.into())
+                    Err(anyhow!("Interpreter process {interpreter} exited abnormally. Exit code: {exit_code}"))
=                 }
=            })
=    }
@@ -163,14 +165,14 @@ impl<'a> ScenarioReport<'a> {
=    fn send(
=        writer: &mut LineWriter<std::process::ChildStdin>,
=        message: &ControlMessage,
-    ) -> Result<(), Box<dyn Error>> {
+    ) -> anyhow::Result<()> {
=        let json = serde_json::to_string(&message)?;
=        Self::write_line(writer, &json)
=    }
=
=    fn receive(
=        reader: &mut BufReader<std::process::ChildStdout>,
-    ) -> Result<InterpreterMessage, Box<dyn Error>> {
+    ) -> anyhow::Result<InterpreterMessage> {
=        let buffer = Self::read_line(reader)?;
=        let message = serde_json::from_str(&buffer)?;
=
@@ -180,7 +182,7 @@ impl<'a> ScenarioReport<'a> {
=    fn write_line(
=        writer: &mut LineWriter<std::process::ChildStdin>,
=        line: &str,
-    ) -> Result<(), Box<dyn Error>> {
+    ) -> anyhow::Result<()> {
=        let buffer = [line, "\n"].concat();
=
=        log::debug!("Sending: {}", buffer);
@@ -188,12 +190,10 @@ impl<'a> ScenarioReport<'a> {
=        Ok(())
=    }
=
-    fn read_line(
-        reader: &mut BufReader<std::process::ChildStdout>,
-    ) -> Result<String, Box<dyn Error>> {
+    fn read_line(reader: &mut BufReader<std::process::ChildStdout>) -> anyhow::Result<String> {
=        let mut buffer = String::new();
=        if reader.read_line(&mut buffer)? == 0 {
-            return Err("Can't read from the interpreter process.".into());
+            return Err(anyhow!("Can't read from the interpreter process."));
=        };
=        Ok(buffer)
=    }
@@ -213,7 +213,7 @@ pub enum StepStatus {
=pub enum ScenarioStatus {
=    Done,
=    Pending,
-    FailedToRun { error: Box<dyn Error> },
+    FailedToRun { error: anyhow::Error },
=}
=
=impl Display for EvaluationReport<'_> {
index 4249efd..f58e607 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -1,5 +1,5 @@
+use anyhow::anyhow;
=use serde::{Deserialize, Serialize};
-use std::error::Error;
=use std::fmt::Display;
=
=/// Spec is a collection of suites that together describe a system
@@ -13,7 +13,7 @@ pub struct Spec {
=
=impl Spec {
=    /// Load suites from a markdown document
-    pub fn load_document(&mut self, md: &str) -> Result<(), Box<dyn Error>> {
+    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)?;
=
@@ -98,7 +98,7 @@ pub struct FrontMatter {
=}
=
=impl Suite {
-    pub fn from_markdown(md: &str) -> Result<Self, Box<dyn Error>> {
+    pub fn from_markdown(md: &str) -> anyhow::Result<Self> {
=        let mdast = markdown::to_mdast(
=            md,
=            &markdown::ParseOptions {
@@ -109,7 +109,9 @@ impl Suite {
=                ..Default::default()
=            },
=        )
-        .map_err(|message| message.to_string())?;
+        .map_err(|message| {
+            anyhow!("Failed to parse markdown input").context(message.to_string())
+        })?;
=        log::debug!("Markdown parsed:\n\n{:#?}", mdast);
=
=        Self::try_from(mdast)
@@ -117,17 +119,29 @@ impl Suite {
=}
=
=impl TryFrom<markdown::mdast::Node> for Suite {
-    type Error = Box<dyn Error>;
+    type Error = anyhow::Error;
=
-    fn try_from(mdast: markdown::mdast::Node) -> Result<Self, Self::Error> {
+    fn try_from(mdast: markdown::mdast::Node) -> anyhow::Result<Self> {
=        // Find the YAML front-matter and extract the interpreter field
-        let children = mdast.children().ok_or("The given node has no children.")?;
+        let children = mdast
+            .children()
+            .ok_or(anyhow!("The given node has no children."))?;
=        let first = children
=            .first()
-            .ok_or("There is no first child in the given node.")?;
+            .ok_or(anyhow!("There is no first child in the given node."))?;
=
=        let markdown::mdast::Node::Yaml(markdown::mdast::Yaml { value: yaml, .. }) = first else {
-            return Err("First child is not a YAML front matter.".into());
+            return Err(
+                (anyhow!("First child is not a YAML front matter.")) .context(concat!(
+                    "At the top of your markdown document you need to provide a YAML front-matter that looks something like this:\n",
+                    "\n",
+                    "---\n",
+                    "interpreter: ./my-interpreter.sh --any arguments you like\n",
+                    "---\n",
+                    "\n",
+                    "The rest of the markdown should follow.\n"
+                )),
+            );
=        };
=        let frontmatter: FrontMatter = serde_yaml::from_str(yaml)?;
=
@@ -145,7 +159,9 @@ impl TryFrom<markdown::mdast::Node> for Suite {
=                }
=            })
=            .next()
-            .ok_or("There is no level 1 heading inside the given node.")?
+            .ok_or(anyhow!(
+                "There is no level 1 heading inside the given node."
+            ))?
=            .to_string();
=
=        // Extract scenarios and steps
@@ -195,19 +211,23 @@ impl TryFrom<markdown::mdast::Node> for Suite {
=}
=
=impl TryFrom<&markdown::mdast::ListItem> for Step {
-    type Error = Box<dyn Error>;
+    type Error = anyhow::Error;
=
=    fn try_from(item: &markdown::mdast::ListItem) -> Result<Self, Self::Error> {
=        let headline = item
=            .children
=            .first()
-            .ok_or("Encountered a list item without any children. Impossible?")?
+            .ok_or(anyhow!(
+                "Encountered a list item without any children. Impossible?"
+            ))?
=            .clone();
=        let mut arguments = Vec::new();
=        let mut variant_headline = headline.clone();
=        for child in variant_headline
=            .children_mut()
-            .ok_or("Encountered a list heading with an empty first child. Impossible?")?
+            .ok_or(anyhow!(
+                "Encountered a list heading with an empty first child. Impossible?"
+            ))?
=            .iter_mut()
=        {
=            if let markdown::mdast::Node::InlineCode(inline_code) = child {

Provide more context when failing

Using anyhow context feature.

index 2811a83..177a8fb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -179,6 +179,15 @@ dependencies = [
= "hashbrown",
=]
=
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
=[[package]]
=name = "is_terminal_polyfill"
=version = "1.70.2"
@@ -304,6 +313,12 @@ version = "0.8.8"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
=
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
=[[package]]
=name = "ryu"
=version = "1.0.20"
@@ -392,6 +407,7 @@ dependencies = [
= "colored",
= "env_logger",
= "glob",
+ "indoc",
= "log",
= "markdown",
= "serde",
index c76fbd2..7e168bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ clap = { version = "4.5.51", features = ["derive"] }
=colored = "3.0.0"
=env_logger = "0.11.8"
=glob = "0.3.3"
+indoc = "2.0.7"
=log = "0.4.28"
=markdown = "1.0.0"
=serde = { version = "1.0.228", features = ["serde_derive"] }
index b5da4fa..1e3dab7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,13 @@
=mod report;
=mod spec;
=
+use anyhow::{Context, Error, bail};
=use clap::Parser;
=use env_logger;
=use glob::glob;
=use log;
=use report::EvaluationReport;
=use spec::Spec;
-use std::error::Error;
=use std::path::PathBuf;
=
=#[derive(Parser)]
@@ -22,7 +22,7 @@ struct Cli {
=    verbose: bool,
=}
=
-fn main() -> Result<(), Box<dyn Error>> {
+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" });
@@ -41,15 +41,20 @@ fn main() -> Result<(), Box<dyn Error>> {
=        let pattern = format!("{}/**/*.md", input.display());
=
=        for path in glob(&pattern)? {
-            let md = std::fs::read_to_string(path?)?;
-            spec.load_document(&md)?;
+            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)?;
-        spec.load_document(&md)?;
+        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 {
-        return Err(format!("The {} is neither a file nor directory", input.display()).into());
+        bail!("The {} is neither a file nor directory", input.display());
=    };
=
=    log::info!("Collected {} suites.", spec.suites.len());
index a187408..5ccc2b2 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -1,6 +1,7 @@
=use crate::spec::{Scenario, Spec, Step, Suite};
-use anyhow::anyhow;
+use anyhow::{Context, anyhow};
=use colored::Colorize;
+use indoc::formatdoc;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
=use std::io::{BufRead, BufReader, LineWriter, Write};
@@ -41,7 +42,9 @@ impl<'a> SuiteReport<'a> {
=        log::debug!("Evaluating suite {}", self.suite.title);
=
=        for scenario in self.scenarios.iter_mut() {
-            let result = scenario.run(&self.suite.interpreter, verbose);
+            let result = scenario
+                .run(&self.suite.interpreter, verbose)
+                .context(format!("running scenario: {}", scenario.scenario.title));
=            if let Err(error) = result {
=                scenario.status = ScenarioStatus::FailedToRun { error }
=            } else {
@@ -73,18 +76,19 @@ impl<'a> ScenarioReport<'a> {
=                "tbb_interpreter_log",
=                if verbose { "debug" } else { "info" },
=            )
-            .spawn()?;
+            .spawn()
+            .context(format!("spawning interpreter from {interpreter}"))?;
=
=        let Some(input) = process.stdin.take() else {
=            // TODO: Avoid string errors!
=            return Err(anyhow!(
-                "Failed to take the stdin stream of the interpreter process."
+                "failed to take the stdin stream of the interpreter process"
=            ));
=        };
=
=        let Some(output) = process.stdout.take() else {
=            return Err(anyhow!(
-                "Failed to take the stdout stream of the interpreter process."
+                "failed to take the stdout stream of the interpreter process"
=            ));
=        };
=
@@ -92,11 +96,11 @@ impl<'a> ScenarioReport<'a> {
=        let mut writer = LineWriter::new(input);
=
=        loop {
-            let ready = match Self::receive(&mut reader)? {
+            let ready = match Self::receive(&mut reader).context("awaiting for the interpreter")? {
=                InterpreterMessage::InterpreterState { ready } => ready,
=                message => {
=                    return Err(anyhow!(
-                        "Unexpected message received from the interpreter: {message:#?}"
+                        "unexpected message received from the interpreter: {message:#?}"
=                    ));
=                }
=            };
@@ -139,7 +143,7 @@ impl<'a> ScenarioReport<'a> {
=                }
=                unexpected => {
=                    return Err(anyhow!(
-                        "Unexpected message received from the interpreter: {unexpected:#?}"
+                        "unexpected message received from the interpreter: {unexpected:#?}"
=                    ));
=                }
=            };
@@ -149,15 +153,20 @@ impl<'a> ScenarioReport<'a> {
=
=        process
=            .wait()
-            .map_err(|io_error| { anyhow::Error::from(io_error) })
+            .map_err(|io_error| anyhow::Error::from(io_error))
=            .and_then(|exit_status| {
=                log::debug!("Interpreter closed with exit status {exit_status:?}");
=                let exit_code = exit_status.code().unwrap_or(0); // Is that right?
=                if exit_code == 0 {
=                    Ok(())
=                } else {
-                    Err(anyhow!("Interpreter process {interpreter} exited abnormally. Exit code: {exit_code}"))
-                 }
+                    Err(anyhow!(formatdoc! {r#"
+                        interpreter process  exited abnormally
+
+                        Interpreter: {interpreter}
+                        Exit code: {exit_code}"
+                    "#}))
+                }
=            })
=    }
=
@@ -193,7 +202,7 @@ impl<'a> ScenarioReport<'a> {
=    fn read_line(reader: &mut BufReader<std::process::ChildStdout>) -> anyhow::Result<String> {
=        let mut buffer = String::new();
=        if reader.read_line(&mut buffer)? == 0 {
-            return Err(anyhow!("Can't read from the interpreter process."));
+            return Err(anyhow!("can't read from the interpreter process"));
=        };
=        Ok(buffer)
=    }
@@ -239,7 +248,11 @@ impl Display for EvaluationReport<'_> {
=                };
=                writeln!(f, "\n  {sigil} {title}\n", title = scenario.title)?;
=                if let ScenarioStatus::FailedToRun { error } = status {
-                    writeln!(f, "    {}\n", error.to_string().red())?;
+                    writeln!(f, "    {}\n", error.root_cause().to_string().red())?;
+                    for cause in error.chain() {
+                        writeln!(f, "      * {}", cause)?;
+                    }
+                    writeln!(f, "\n")?;
=                }
=
=                for StepReport { step, status } in steps.iter() {
index f58e607..9da0529 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -1,4 +1,5 @@
-use anyhow::anyhow;
+use anyhow::{Context, anyhow, bail};
+use indoc::indoc;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
=
@@ -15,7 +16,7 @@ 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)?;
+        let suite = Suite::from_markdown(md).context("loading a markdown document")?;
=
=        self.suites.push(suite);
=        Ok(())
@@ -110,11 +111,11 @@ impl Suite {
=            },
=        )
=        .map_err(|message| {
-            anyhow!("Failed to parse markdown input").context(message.to_string())
+            anyhow!("failed to parse markdown input").context(message.to_string())
=        })?;
=        log::debug!("Markdown parsed:\n\n{:#?}", mdast);
=
-        Self::try_from(mdast)
+        Self::try_from(mdast).context("extracting a suite from a markdown document")
=    }
=}
=
@@ -123,27 +124,24 @@ impl TryFrom<markdown::mdast::Node> for Suite {
=
=    fn try_from(mdast: markdown::mdast::Node) -> anyhow::Result<Self> {
=        // Find the YAML front-matter and extract the interpreter field
-        let children = mdast
-            .children()
-            .ok_or(anyhow!("The given node has no children."))?;
-        let first = children
-            .first()
-            .ok_or(anyhow!("There is no first child in the given node."))?;
+        let children = mdast.children().context("the markdown document is empty")?;
+        let first = children.first().context("the markdown document is empty")?;
=
=        let markdown::mdast::Node::Yaml(markdown::mdast::Yaml { value: yaml, .. }) = first else {
-            return Err(
-                (anyhow!("First child is not a YAML front matter.")) .context(concat!(
-                    "At the top of your markdown document you need to provide a YAML front-matter that looks something like this:\n",
-                    "\n",
-                    "---\n",
-                    "interpreter: ./my-interpreter.sh --any arguments you like\n",
-                    "---\n",
-                    "\n",
-                    "The rest of the markdown should follow.\n"
-                )),
-            );
+            bail!(indoc! {r#"
+                missing YAML front-matter
+ 
+                At the top of your markdown document you need to provide a YAML front-matter that looks something like this:
+
+                ---
+                interpreter: ./my-interpreter.sh --any arguments you like
+                ---
+
+                The rest of the markdown should follow.
+            "#});
=        };
-        let frontmatter: FrontMatter = serde_yaml::from_str(yaml)?;
+        let frontmatter: FrontMatter =
+            serde_yaml::from_str(yaml).context("Failed to parse front-matter as YAML")?;
=
=        // Find h1 and use it as title (
=        // TODO: make sure there's only one and it's before any h2
@@ -159,9 +157,15 @@ impl TryFrom<markdown::mdast::Node> for Suite {
=                }
=            })
=            .next()
-            .ok_or(anyhow!(
-                "There is no level 1 heading inside the given node."
-            ))?
+            .context(indoc! {r#"
+                no h1 heading found
+
+                Each markdown document must have at least one h1 heading, like this:
+
+                # Some important aspect of the spec
+
+                This will be used as a title of a suite.
+            "#})?
=            .to_string();
=
=        // Extract scenarios and steps
@@ -217,17 +221,13 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
=        let headline = item
=            .children
=            .first()
-            .ok_or(anyhow!(
-                "Encountered a list item without any children. Impossible?"
-            ))?
+            .context("a list without children (how is that even possible?)")?
=            .clone();
=        let mut arguments = Vec::new();
=        let mut variant_headline = headline.clone();
=        for child in variant_headline
=            .children_mut()
-            .ok_or(anyhow!(
-                "Encountered a list heading with an empty first child. Impossible?"
-            ))?
+            .context("a list heading with an empty first child (how is that even possible?)")?
=            .iter_mut()
=        {
=            if let markdown::mdast::Node::InlineCode(inline_code) = child {

Bump the patch version

index 177a8fb..3abc786 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.1.0"
+version = "0.1.1"
=dependencies = [
= "anyhow",
= "clap",
index 7e168bb..3601fd4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.1.0"
+version = "0.1.1"
=edition = "2024"
=
=[dependencies]

More context for message passing errors

index 5ccc2b2..4eed1ef 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -175,7 +175,7 @@ impl<'a> ScenarioReport<'a> {
=        writer: &mut LineWriter<std::process::ChildStdin>,
=        message: &ControlMessage,
=    ) -> anyhow::Result<()> {
-        let json = serde_json::to_string(&message)?;
+        let json = serde_json::to_string(&message).context(format!("sending data: {message:?}"))?;
=        Self::write_line(writer, &json)
=    }
=
@@ -183,7 +183,8 @@ impl<'a> ScenarioReport<'a> {
=        reader: &mut BufReader<std::process::ChildStdout>,
=    ) -> anyhow::Result<InterpreterMessage> {
=        let buffer = Self::read_line(reader)?;
-        let message = serde_json::from_str(&buffer)?;
+        let message =
+            serde_json::from_str(&buffer).context(format!("received string: {buffer:?}"))?;
=
=        Ok(message)
=    }
@@ -195,7 +196,9 @@ impl<'a> ScenarioReport<'a> {
=        let buffer = [line, "\n"].concat();
=
=        log::debug!("Sending: {}", buffer);
-        writer.write_all(buffer.as_bytes())?;
+        writer
+            .write_all(buffer.as_bytes())
+            .context(format!("writing data: {buffer:?}"))?;
=        Ok(())
=    }
=

Bump the patch version

index 3abc786..922de52 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.1.1"
+version = "0.1.2"
=dependencies = [
= "anyhow",
= "clap",
index 3601fd4..b7c5dcd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.1.1"
+version = "0.1.2"
=edition = "2024"
=
=[dependencies]