Commits: 9
Write steps to verify source information passing
index 9b1af63..ce81686 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -179,4 +179,16 @@ def step_implementation_13(row: int, column: int, table_name: str, expected_valu
= table = state[table_name]
= tester.assertEqual(table[row][column], expected_value)
=
+@step("This step is defined between lines {0} and {1} in {2}")
+def step_implementation_14(first_line: int, last_line: int, document_path: str, **kwargs):
+ tester.assertEqual(
+ {
+ "first_line": first_line,
+ "last_line": last_line,
+ "document_path": document_path
+ },
+ kwargs["source"]
+ )
+
+
=tbb.ready()index 19f98e2..76b8358 100644
--- a/spec/source-information.md
+++ b/spec/source-information.md
@@ -22,4 +22,11 @@ When executing a step, the interpreter will get it's source file path and lines
=
=This is to fulfill the "batteries included, but replaceable" principle. With this information, an interpreter can extract any additional information from a step.
=
+## Source information will be passed to interpreter
=
+ * This step is defined between lines `27` and `30` in `spec/source-information.md`
+
+ Every step is passed with `source` property, conatining `first_line`, `last_line` and `document_path`
+
+ * This step is defined between lines `31` and `31` in `spec/source-information.md`
+ * This step is defined between lines `32` and `32` in `spec/source-information.md`Implement the source information passing
index b37e0a9..2a1b8e0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -77,16 +77,16 @@ fn list(input: PathBuf) -> Result<(), Error> {
=
= for path in glob(&pattern)? {
= let path = path.context(format!("resolving {pattern}"))?;
- let md = std::fs::read_to_string(&path)
+ let md = std::fs::read_to_string(&path.clone())
= .context(format!("reading a file at {}", path.display()))?;
- spec.load_document(&md)
+ spec.load_document(relative_path(&path), &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)
+ spec.load_document(relative_path(&input), &md)
= .context(format!("loading a document from {}", input.display()))?;
= } else {
= bail!("The {} is neither a file nor directory", input.display());
@@ -119,14 +119,14 @@ fn evaluate(
= 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)
+ spec.load_document(relative_path(&path), &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)
+ spec.load_document(relative_path(&input), &md)
= .context(format!("loading a document from {}", input.display()))?;
= } else {
= bail!("The {} is neither a file nor directory", input.display());
@@ -204,3 +204,16 @@ fn evaluate(
= }
= }
=}
+
+fn relative_path(path: &PathBuf) -> PathBuf {
+ let document_path: PathBuf = std::env::current_dir()
+ .context("Getting current working directory")
+ .and_then(|cwd| {
+ log::info!("Stripping prefix {}", cwd.display());
+ path.strip_prefix(cwd)
+ .context("Resolving a relative path to a document")
+ })
+ .map(PathBuf::from)
+ .unwrap_or(PathBuf::from("unknown"));
+ document_path
+}index 885e143..4ec607b 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
=use std::collections::BTreeSet;
=use std::fmt::Display;
=use std::ops::Not;
+use std::path::PathBuf;
=
=/// Spec is a collection of suites that together describe a system
=///
@@ -30,8 +31,9 @@ pub struct Spec {
=pub struct Suite {
= pub title: String,
= pub tags: BTreeSet<String>,
- pub interpreter: String,
+ pub interpreter: String, // TODO: Interpreter should be a property of a scenario?
= pub scenarios: Vec<Scenario>,
+ pub source: SourceInfromation,
=}
=
=/// Scenario is set of steps.
@@ -45,6 +47,7 @@ pub struct Scenario {
= pub title: String,
= pub tags: BTreeSet<String>,
= pub steps: Vec<Step>,
+ pub source: SourceInfromation,
=}
=
=#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -58,6 +61,9 @@ pub struct Step {
= /// Values of the arguments
= pub arguments: Vec<String>,
=
+ /// Source information
+ pub source: SourceInfromation,
+
= /// List of code blocks
= pub code_blocks: Vec<CodeBlock>,
=
@@ -68,6 +74,13 @@ pub struct Step {
= pub lists: Vec<List>,
=}
=
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct SourceInfromation {
+ pub document_path: PathBuf,
+ pub first_line: usize,
+ pub last_line: usize,
+}
+
=#[derive(Debug, Serialize, Deserialize, Clone)]
=pub struct CodeBlock {
= value: String,
@@ -88,9 +101,10 @@ pub struct ListItem {
=
=impl Spec {
= /// Load suites from a markdown document
- pub fn load_document(&mut self, md: &str) -> anyhow::Result<()> {
+ pub fn load_document(&mut self, document_path: PathBuf, 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")?;
+ let suite =
+ Suite::from_markdown(document_path, md).context("loading a markdown document")?;
=
= self.suites.push(suite);
= Ok(())
@@ -258,7 +272,7 @@ pub struct Metadata {
=}
=
=impl Suite {
- pub fn from_markdown(md: &str) -> anyhow::Result<Self> {
+ pub fn from_markdown(document_path: PathBuf, md: &str) -> anyhow::Result<Self> {
= let mdast = markdown::to_mdast(
= md,
= &markdown::ParseOptions {
@@ -275,14 +289,6 @@ impl Suite {
= })?;
= log::debug!("Markdown parsed:\n\n{:#?}", mdast);
=
- Self::try_from(mdast).context("extracting a suite from a markdown document")
- }
-}
-
-impl TryFrom<markdown::mdast::Node> for Suite {
- type Error = anyhow::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().context("the markdown document is empty")?;
= let first = children.first().context("the markdown document is empty")?;
@@ -304,8 +310,8 @@ impl TryFrom<markdown::mdast::Node> for Suite {
= 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
- let title = children
+
+ let title_node = children
= .iter()
= .filter(|element| {
= if let markdown::mdast::Node::Heading(heading) = element
@@ -325,8 +331,22 @@ impl TryFrom<markdown::mdast::Node> for Suite {
= # Some important aspect of the spec
=
= This will be used as a title of a suite.
- "#})?
- .to_string();
+ "#})?;
+
+ let source = SourceInfromation {
+ document_path: document_path.clone(),
+ first_line: title_node
+ .position()
+ .context("Getting the source line on which the suite starts")?
+ .start
+ .line,
+ last_line: mdast
+ .position()
+ .context("Getting the last source line of the document")?
+ .end
+ .line,
+ };
+ let title = title_node.to_string();
=
= // Find any yaml meta code block after the h1 but before any h2
=
@@ -335,15 +355,32 @@ impl TryFrom<markdown::mdast::Node> for Suite {
= // Extract scenarios and steps
= // Split into sections, each starting at h2
= // Convert each section into a scenario
- let mut scenarios = Vec::new();
+ let mut scenarios: Vec<Scenario> = Vec::new();
= for node in children.iter() {
= match node {
= markdown::mdast::Node::Heading(heading) => {
= if heading.depth == 2 {
+ let line = node
+ .position()
+ .context("Getting the source line where the scenario starts")?
+ .start
+ .line;
+
+ // Mark the end of previous scenario, if any
+ if let Some(previous_scenario) = scenarios.last_mut() {
+ previous_scenario.source.last_line = line - 1;
+ }
+
= scenarios.push(Scenario {
= title: node.to_string(),
= tags: BTreeSet::default(),
= steps: [].into(),
+ source: SourceInfromation {
+ document_path: document_path.clone(),
+ first_line: line,
+ // Assume the scenario goes until the end of document, until proven otherwise
+ last_line: source.last_line,
+ },
= });
= }
= }
@@ -359,7 +396,7 @@ impl TryFrom<markdown::mdast::Node> for Suite {
=
= for item in items {
= // First child of a list item should always be a paragraph
- let step = Step::try_from(item)?;
+ let step = Step::from_markdown(document_path.clone(), item)?;
=
= scenario.steps.push(step);
= }
@@ -397,14 +434,32 @@ impl TryFrom<markdown::mdast::Node> for Suite {
= tags: suite_tags,
= scenarios,
= interpreter: frontmatter.interpreter,
+ source,
= })
= }
=}
=
-impl TryFrom<&markdown::mdast::ListItem> for Step {
- type Error = anyhow::Error;
+impl Step {
+ fn from_markdown(
+ document_path: PathBuf,
+ item: &markdown::mdast::ListItem,
+ ) -> Result<Self, anyhow::Error> {
+ let source = SourceInfromation {
+ document_path,
+ first_line: item
+ .clone()
+ .position
+ .context("Getting first line of the step list item")?
+ .start
+ .line,
+ last_line: item
+ .clone()
+ .position
+ .context("Getting last line of the step list item")?
+ .end
+ .line,
+ };
=
- fn try_from(item: &markdown::mdast::ListItem) -> Result<Self, Self::Error> {
= let headline = item
= .children
= .first()
@@ -463,6 +518,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= description: headline.to_string(),
= variant: variant_headline.to_string(),
= arguments,
+ source,
= code_blocks,
= tables,
= lists,Spec: Rename list sub-command to show
Change the output format.
index c117e8b..7762c58 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -15,6 +15,9 @@ We use this document mostly to keep track of our progress and a scratchpad for i
= - [ ] Cucumber
= - [ ] Playwright
=- [ ] Demo
+- [ ] Show and report output formats
+ - [ ] JSON
+ - [ ] Markdown
=- [x] Proof of concept
= - [x] Interpretter in a different language (Python)
= - [x] Report why steps fail
@@ -22,7 +25,7 @@ We use this document mostly to keep track of our progress and a scratchpad for i
= - [x] Proof of concept
= - [ ] Comprehensive
= - [x] Help
- - [x] List
+ - [x] Show
= - [x] Evaluate
= - [ ] Tagging
= - [x] Filteringindex fb1caf7..d93816d 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -24,7 +24,7 @@ interpreter: "python -m spec.self-check"
=
= ``` text
= Commands:
- list Print the suites, scenarios and steps of the specification
+ show Print the suites, scenarios and steps of the specification
= evaluate Evaluate the specification
= help Print this message or the help of the given subcommand(s)
= ```
@@ -38,7 +38,7 @@ interpreter: "python -m spec.self-check"
= * The exit code should be `0`
=
=
-## Listing a spec from the `./spec/` directory
+## Showing a spec from the `./spec/` directory
=
=This is the default behavior when a path is not given. We should prepare the samples to replicate this directory structure like this
=
@@ -56,19 +56,21 @@ samples/python/
=We would also need a step like `Change working directory to ...` to execute before running the program.
=
=
-## Listing a spec from a different directory
+## Showing a spec from a different directory
=
=Whe a directory is given as the last argument, load all documents inside (recursively).
=
=
-## Listing suites and scenarios from a single document
+## Showing suites and scenarios from a single document
=
- * Run the program with `list samples/basic.md` command line arguments
+ * Run the program with `show samples/basic.md` command line arguments
= * The output will contain `the suite` block
=
= ``` text
- Basic BDD suite (python -m samples.basic)
+ Basic BDD suite
= tagged: basic not-implemented sample tutorial
+ interpreter: python -m samples.basic
+ source: samples/basic.md
= ```
=
= * The output will contain `the Aritmetic scenario` block
@@ -76,10 +78,17 @@ Whe a directory is given as the last argument, load all documents inside (recurs
= ``` text
= * Arithmetic
= tagged: math
-
- 00. Add 7 and 5 to get 12 ["7", "5", "12"]
- 01. Divide 10 by 4 to get 2.5 ["10", "4", "2.5"]
- 02. Subtract 7 from 5 to get -2 ["7", "5", "-2"]
+ source: samples/basic.md:22-34
+
+ 00. Add 7 and 5 to get 12
+ arguments: ["7", "5", "12"]
+ source: samples/basic.md:30-30
+ 01. Divide 10 by 4 to get 2.5
+ arguments: ["10", "4", "2.5"]
+ source: samples/basic.md:31-31
+ 02. Subtract 7 from 5 to get -2
+ arguments: ["7", "5", "-2"]
+ source: samples/basic.md:32-34
= ```
=
= * The output will contain `the Text scenario` block
@@ -87,14 +96,29 @@ Whe a directory is given as the last argument, load all documents inside (recurs
= ``` text
= * Text
= tagged: strings work-in-progress
-
- 00. The word blocks has 6 characters ["blocks", "6"]
- 01. There are 3 properties in the following JSON ["3"]
- 02. There are 3 rs in the word strawberry ["3", "r", "strawberry"]
- 03. The following table maps words to their lengths []
- 04. The reverse of abc is cba ["abc", "cba"]
- 05. The reverse of CIA is KGB ["CIA", "KGB"]
- 06. There are 2 os in the word boost ["2", "o", "boost"]
+ source: samples/basic.md:35-91
+
+ 00. The word blocks has 6 characters
+ arguments: ["blocks", "6"]
+ source: samples/basic.md:45-45
+ 01. There are 3 properties in the following JSON
+ arguments: ["3"], code blocks: 1
+ source: samples/basic.md:46-57
+ 02. There are 3 rs in the word strawberry
+ arguments: ["3", "r", "strawberry"]
+ source: samples/basic.md:58-58
+ 03. The following table maps words to their lengths
+ code blocks: 1, tables: 1
+ source: samples/basic.md:59-84
+ 04. The reverse of abc is cba
+ arguments: ["abc", "cba"]
+ source: samples/basic.md:85-85
+ 05. The reverse of CIA is KGB
+ arguments: ["CIA", "KGB"]
+ source: samples/basic.md:86-89
+ 06. There are 2 os in the word boost
+ arguments: ["2", "o", "boost"]
+ source: samples/basic.md:90-91
= ```
=
= * The exit code should be `0`
@@ -130,7 +154,7 @@ A complete sample output is like this:
= □ 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 `□`.
+Notice it's similar to the output of `tbb show`, 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 `evaluate samples/basic.md` command line argumentsRename the list command to show, change display format
As specified.
index 2a1b8e0..c398fe0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,7 +25,7 @@ struct Cli {
=#[derive(Subcommand)]
=enum Command {
= /// Print the suites, scenarios and steps of the specification
- List {
+ Show {
= /// A directory or a markdown file with the spec to list
= #[arg(value_name = "SPEC PATH", default_value = "./spec/")]
= input: PathBuf,
@@ -53,7 +53,7 @@ fn main() -> Result<(), Error> {
= env_logger::init_from_env(log_env);
=
= match cli.command {
- Command::List { input } => list(input),
+ Command::Show { input } => show(input),
= Command::Evaluate {
= input,
= only,
@@ -62,7 +62,7 @@ fn main() -> Result<(), Error> {
= }
=}
=
-fn list(input: PathBuf) -> Result<(), Error> {
+fn show(input: PathBuf) -> Result<(), Error> {
= log::debug!("Reading the specification from {}", input.display());
=
= let input = input.canonicalize()?;index 4ec607b..84a262e 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -3,7 +3,7 @@ use anyhow::{Context, anyhow, bail};
=use colored::Colorize;
=use indoc::indoc;
=use serde::{Deserialize, Serialize};
-use std::collections::BTreeSet;
+use std::collections::{BTreeMap, BTreeSet};
=use std::fmt::Display;
=use std::ops::Not;
=use std::path::PathBuf;
@@ -167,12 +167,7 @@ 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.bold().underline(),
- interpreter = suite.interpreter.dimmed()
- )?;
+ writeln!(f, "\n{title}", title = suite.title.bold().underline(),)?;
=
= if suite.tags.is_empty().not() {
= let tags = suite
@@ -184,6 +179,21 @@ impl Display for Spec {
= writeln!(f, "tagged: {tags}")?;
= }
=
+ let interpreter = format!(
+ "interpreter: {interpreter}",
+ interpreter = suite.interpreter
+ )
+ .dimmed();
+ writeln!(f, "{interpreter}")?;
+
+ // Source of the suite
+ let source = format!(
+ "source: {source}",
+ source = suite.source.document_path.display()
+ )
+ .dimmed();
+ writeln!(f, "{source}")?;
+
= for scenario in suite.scenarios.iter() {
= writeln!(
= f,
@@ -191,6 +201,7 @@ impl Display for Spec {
= title = scenario.title.bold(),
= indentation = "".indent(2)
= )?;
+
= if scenario.tags.is_empty().not() {
= let tags = scenario
= .tags
@@ -200,15 +211,60 @@ impl Display for Spec {
= .join(" ");
= writeln!(f, "{indentation}tagged: {tags}", indentation = "".indent(4))?;
= }
+
+ // Source of the scenario
+ let source = format!(
+ "source: {document_path}:{first_line}-{last_line}",
+ document_path = scenario.source.document_path.display(),
+ first_line = scenario.source.first_line,
+ last_line = scenario.source.last_line,
+ )
+ .indent(4)
+ .dimmed();
+ writeln!(f, "{source}")?;
= writeln!(f, "")?;
=
= for (index, step) in scenario.steps.iter().enumerate() {
= writeln!(
= f,
- " {index:02}. {description} {arguments}",
+ " {index:02}. {description}",
= description = step.description,
- arguments = format!("{:?}", step.arguments).dimmed()
= )?;
+
+ // Arguments, code blocks, lists and tables
+ let mut data_objects = BTreeMap::from([
+ ("code blocks", step.code_blocks.len()),
+ ("lists", step.lists.len()),
+ ("tables", step.tables.len()),
+ ])
+ .iter()
+ .filter_map(|(kind, count)| {
+ if *count == 0 {
+ None
+ } else {
+ Some(format!("{kind}: {count}"))
+ }
+ })
+ .collect::<Vec<String>>();
+
+ if step.arguments.is_empty().not() {
+ data_objects.insert(0, format!("arguments: {:?}", step.arguments));
+ };
+
+ if data_objects.is_empty().not() {
+ writeln!(f, "{}", data_objects.join(", ").indent(8).dimmed())?;
+ }
+
+ // Source of the step
+ let source = format!(
+ "source: {document_path}:{first_line}-{last_line}",
+ document_path = step.source.document_path.display(),
+ first_line = step.source.first_line,
+ last_line = step.source.last_line,
+ )
+ .indent(8)
+ .dimmed();
+ writeln!(f, "{source}")?;
= }
= }
= writeln!(f, "")?;Refactor spec display: delegate to suite, scenario, step
I want to re-use this logic in report display.
index c398fe0..d674ae7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -209,7 +209,6 @@ fn relative_path(path: &PathBuf) -> PathBuf {
= let document_path: PathBuf = std::env::current_dir()
= .context("Getting current working directory")
= .and_then(|cwd| {
- log::info!("Stripping prefix {}", cwd.display());
= path.strip_prefix(cwd)
= .context("Resolving a relative path to a document")
= })index 84a262e..955e225 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -167,104 +167,14 @@ 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}", title = suite.title.bold().underline(),)?;
-
- if suite.tags.is_empty().not() {
- let tags = suite
- .tags
- .iter()
- .map(|tag| tag.underline().to_string())
- .collect::<Vec<String>>()
- .join(" ");
- writeln!(f, "tagged: {tags}")?;
- }
-
- let interpreter = format!(
- "interpreter: {interpreter}",
- interpreter = suite.interpreter
- )
- .dimmed();
- writeln!(f, "{interpreter}")?;
-
- // Source of the suite
- let source = format!(
- "source: {source}",
- source = suite.source.document_path.display()
- )
- .dimmed();
- writeln!(f, "{source}")?;
+ writeln!(f, "\n{suite}")?;
=
= for scenario in suite.scenarios.iter() {
- writeln!(
- f,
- "\n{indentation}* {title}",
- title = scenario.title.bold(),
- indentation = "".indent(2)
- )?;
-
- if scenario.tags.is_empty().not() {
- let tags = scenario
- .tags
- .iter()
- .map(|tag| tag.underline().to_string())
- .collect::<Vec<String>>()
- .join(" ");
- writeln!(f, "{indentation}tagged: {tags}", indentation = "".indent(4))?;
- }
-
- // Source of the scenario
- let source = format!(
- "source: {document_path}:{first_line}-{last_line}",
- document_path = scenario.source.document_path.display(),
- first_line = scenario.source.first_line,
- last_line = scenario.source.last_line,
- )
- .indent(4)
- .dimmed();
- writeln!(f, "{source}")?;
+ writeln!(f, "\n * {}", scenario.to_string().indent(4).trim())?;
= writeln!(f, "")?;
=
= for (index, step) in scenario.steps.iter().enumerate() {
- writeln!(
- f,
- " {index:02}. {description}",
- description = step.description,
- )?;
-
- // Arguments, code blocks, lists and tables
- let mut data_objects = BTreeMap::from([
- ("code blocks", step.code_blocks.len()),
- ("lists", step.lists.len()),
- ("tables", step.tables.len()),
- ])
- .iter()
- .filter_map(|(kind, count)| {
- if *count == 0 {
- None
- } else {
- Some(format!("{kind}: {count}"))
- }
- })
- .collect::<Vec<String>>();
-
- if step.arguments.is_empty().not() {
- data_objects.insert(0, format!("arguments: {:?}", step.arguments));
- };
-
- if data_objects.is_empty().not() {
- writeln!(f, "{}", data_objects.join(", ").indent(8).dimmed())?;
- }
-
- // Source of the step
- let source = format!(
- "source: {document_path}:{first_line}-{last_line}",
- document_path = step.source.document_path.display(),
- first_line = step.source.first_line,
- last_line = step.source.last_line,
- )
- .indent(8)
- .dimmed();
- writeln!(f, "{source}")?;
+ writeln!(f, " {index:02}. {}", step.to_string().indent(8).trim())?;
= }
= }
= writeln!(f, "")?;
@@ -495,6 +405,60 @@ impl Suite {
= }
=}
=
+impl Display for Suite {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{title}", title = self.title.bold().underline(),)?;
+
+ if self.tags.is_empty().not() {
+ let tags = self
+ .tags
+ .iter()
+ .map(|tag| tag.underline().to_string())
+ .collect::<Vec<String>>()
+ .join(" ");
+ writeln!(f, "tagged: {tags}")?;
+ }
+
+ let interpreter =
+ format!("interpreter: {interpreter}", interpreter = self.interpreter).dimmed();
+ writeln!(f, "{interpreter}")?;
+
+ // Source of the suite
+ let source = format!(
+ "source: {source}",
+ source = self.source.document_path.display()
+ )
+ .dimmed();
+ writeln!(f, "{source}")
+ }
+}
+
+impl Display for Scenario {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{title}", title = self.title.bold(),)?;
+
+ if self.tags.is_empty().not() {
+ let tags = self
+ .tags
+ .iter()
+ .map(|tag| tag.underline().to_string())
+ .collect::<Vec<String>>()
+ .join(" ");
+ writeln!(f, "tagged: {}", tags)?;
+ }
+
+ // Source of the scenario
+ let source = format!(
+ "source: {document_path}:{first_line}-{last_line}",
+ document_path = self.source.document_path.display(),
+ first_line = self.source.first_line,
+ last_line = self.source.last_line,
+ )
+ .dimmed();
+ writeln!(f, "{source}")
+ }
+}
+
=impl Step {
= fn from_markdown(
= document_path: PathBuf,
@@ -581,3 +545,43 @@ impl Step {
= })
= }
=}
+
+impl Display for Step {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{description}", description = self.description,)?;
+
+ // Arguments, code blocks, lists and tables
+ let mut data_objects = BTreeMap::from([
+ ("code blocks", self.code_blocks.len()),
+ ("lists", self.lists.len()),
+ ("tables", self.tables.len()),
+ ])
+ .iter()
+ .filter_map(|(kind, count)| {
+ if *count == 0 {
+ None
+ } else {
+ Some(format!("{kind}: {count}"))
+ }
+ })
+ .collect::<Vec<String>>();
+
+ if self.arguments.is_empty().not() {
+ data_objects.insert(0, format!("arguments: {:?}", self.arguments));
+ };
+
+ if data_objects.is_empty().not() {
+ writeln!(f, "{}", data_objects.join(", ").dimmed())?;
+ }
+
+ // Source of the step
+ let source = format!(
+ "source: {document_path}:{first_line}-{last_line}",
+ document_path = self.source.document_path.display(),
+ first_line = self.source.first_line,
+ last_line = self.source.last_line,
+ )
+ .dimmed();
+ writeln!(f, "{source}")
+ }
+}Make the evaluation report format similar to show
index d93816d..08973b8 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -125,38 +125,6 @@ Whe a directory is given as the last argument, load all documents inside (recurs
=
=## Evaluating a spec from a single document
=
-A complete sample output is like this:
-
-``` text
-
-✓ Arithmetic
- tagged: math
-
- ⊞ 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
- tagged: strings work-in-progress
-
- ⊞ 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 show`, 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 `evaluate samples/basic.md` command line arguments
=
= The `basic.md` suit is intentionally wrong. It should be reflected in the status code.
@@ -164,8 +132,10 @@ Notice it's similar to the output of `tbb show`, but now contains unicode symbol
= * The output will contain `the suite header` block
=
= ```text
- Basic BDD suite (python -m samples.basic)
+ Basic BDD suite
= tagged: basic not-implemented sample tutorial
+ interpreter: python -m samples.basic
+ source: samples/basic.md
= ```
=
= * The output will contain `arithmetic scenario` block
@@ -173,36 +143,65 @@ Notice it's similar to the output of `tbb show`, but now contains unicode symbol
= ```text
= ✓ Arithmetic
= tagged: math
-
- ⊞ 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"]
+ source: samples/basic.md:22-34
+
+ ⊞ Add 7 and 5 to get 12
+ arguments: ["7", "5", "12"]
+ source: samples/basic.md:30-30
+ ⊞ Divide 10 by 4 to get 2.5
+ arguments: ["10", "4", "2.5"]
+ source: samples/basic.md:31-31
+ ⊞ Subtract 7 from 5 to get -2
+ arguments: ["7", "5", "-2"]
+ source: samples/basic.md:32-34
= ```
=
+ Notice it's similar to the output of `tbb show`, but now contains unicode symbols to indicate the results of each step. Each completed scenario has a check mark `✓`.
+
= * The output will contain `text scenario` block
=
= ```text
= ✓ Text
= tagged: strings work-in-progress
-
- ⊞ 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"]
+ source: samples/basic.md:35-91
+
+ ⊞ The word blocks has 6 characters
+ arguments: ["blocks", "6"]
+ source: samples/basic.md:45-45
+ ⊞ There are 3 properties in the following JSON
+ arguments: ["3"], code blocks: 1
+ source: samples/basic.md:46-57
+ ⊞ There are 3 rs in the word strawberry
+ arguments: ["3", "r", "strawberry"]
+ source: samples/basic.md:58-58
+ ⊞ The following table maps words to their lengths
+ code blocks: 1, tables: 1
+ source: samples/basic.md:59-84
+ ⊞ The reverse of abc is cba
+ arguments: ["abc", "cba"]
+ source: samples/basic.md:85-85
+ ⊠ The reverse of CIA is KGB
+ arguments: ["CIA", "KGB"]
+ source: samples/basic.md:86-89
=
= 'KGB' != 'AIC'
=
= - KGB
= + AIC
+
+ □ There are 2 os in the word boost
+ arguments: ["2", "o", "boost"]
+ source: samples/basic.md:90-91
= ```
+
+ 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 `□`.
=
= * The output will contain `geometry scenario` block
=
= ```text
= ? Geometry
= tagged: math
+ source: samples/basic.md:92-99
=
= There are no steps to execute in this scenario.
= ```index c0c4dec..2696fb8 100644
--- a/spec/failing-interpreters.md
+++ b/spec/failing-interpreters.md
@@ -18,15 +18,19 @@ If some interpreters' commands are invalid (e.g. refers to a non-existing progra
= * The output will contain `the valid suite header` block
=
= ```text
- A little suite that could (python -m samples.basic)
+ A little suite that could
= tagged: basic passing
+ interpreter: python -m samples.basic
+ source: samples/some-invalid/valid.md
= ```
=
= * The output will contain `the invalid suite header` block
=
= ```text
- Suite 1 from the invalid document (invalid interpreter)
+ Suite 1 from the invalid document
= tagged: not-implemented seriously-underbaked
+ interpreter: invalid interpreter
+ source: samples/some-invalid/invalid.md
= ```
=
= * The output will contain `the failing scenario` block
@@ -34,6 +38,7 @@ If some interpreters' commands are invalid (e.g. refers to a non-existing progra
= ```text
= x Hopeless scenario
= tagged: even more tags very-important work-in-progress
+ source: samples/some-invalid/invalid.md:17-29
= ```
=
= Notice the failing sigil in front.index e462842..357a1ae 100644
--- a/spec/filtering.md
+++ b/spec/filtering.md
@@ -15,8 +15,10 @@ With the `--only suite:<tag>` only suites that have the given tag should be incl
= * The output will contain `the expected suite header` block
=
= ```text
- A little suite that could (python -m samples.basic)
+ A little suite that could
= tagged: basic passing
+ interpreter: python -m samples.basic
+ source: samples/some-invalid/valid.md
= ```
=
= * The output will not contain `Suite 1 from the invalid document`
@@ -31,8 +33,10 @@ With the `--only scenario:<tag>` only scenarios that have the given tag should b
= * The output will contain `the expected suite header` block
=
= ```text
- A little suite that could (python -m samples.basic)
+ A little suite that could
= tagged: basic passing
+ interpreter: python -m samples.basic
+ source: samples/some-invalid/valid.md
= ```
=
= * The output will contain `the expected scenario header` block
@@ -62,8 +66,10 @@ With the `--exclude suite:<tag>` any suites that have the given tag should be ex
= * The output will contain `the passing suite header` block
=
= ```text
- A little suite that could (python -m samples.basic)
+ A little suite that could
= tagged: basic passing
+ interpreter: python -m samples.basic
+ source: samples/some-invalid/valid.md
= ```
=
= * The output will not contain `Suite 1 from the invalid document`
@@ -77,8 +83,10 @@ With the `--exclude scenario:<tag>` any scenarios that have the given tag should
= * The output will contain `the expected suite header` block
=
= ```text
- A little suite that could (python -m samples.basic)
+ A little suite that could
= tagged: basic passing
+ interpreter: python -m samples.basic
+ source: samples/some-invalid/valid.md
= ```
=
= * The output will contain `the expected scenario header` blockindex 546bbf9..0205116 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -275,22 +275,7 @@ pub enum ScenarioStatus {
=impl Display for EvaluationReport<'_> {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
= for SuiteReport { suite, scenarios } in self.suites.iter() {
- writeln!(
- f,
- "\n{title} ({interpreter})",
- title = suite.title.bold().underline(),
- interpreter = suite.interpreter.dimmed()
- )?;
-
- if suite.tags.is_empty().not() {
- let tags = suite
- .tags
- .iter()
- .map(|tag| tag.underline().to_string())
- .collect::<Vec<String>>()
- .join(" ");
- writeln!(f, "tagged: {tags}",)?;
- }
+ writeln!(f, "\n{suite}")?;
=
= for ScenarioReport {
= scenario,
@@ -303,21 +288,7 @@ impl Display for EvaluationReport<'_> {
= ScenarioStatus::Pending => "?".to_string().bold().dimmed(),
= ScenarioStatus::FailedToRun { .. } => "x".to_string().bold().red(),
= };
- writeln!(
- f,
- "\n{indentation}{sigil} {title}",
- indentation = "".indent(0),
- title = scenario.title.bold()
- )?;
- if scenario.tags.is_empty().not() {
- let tags = scenario
- .tags
- .iter()
- .map(|tag| tag.underline().to_string())
- .collect::<Vec<String>>()
- .join(" ");
- writeln!(f, "{indentation}tagged: {tags}", indentation = "".indent(2))?;
- };
+ writeln!(f, "\n{sigil} {}", scenario.to_string().indent(2).trim())?;
= writeln!(f, "")?;
=
= if let ScenarioStatus::FailedToRun { error } = status {
@@ -350,10 +321,9 @@ impl Display for EvaluationReport<'_> {
= };
= writeln!(
= f,
- "{indentation}{sigil} {description} {arguments}",
+ "{indentation}{sigil} {}",
+ step.to_string().indent(4).trim(),
= indentation = "".indent(2),
- description = step.description,
- arguments = format!("{:?}", step.arguments).dimmed()
= )?;
=
= if let StepStatus::Failed { reason, hint } = status {Condense output when all steps are ok
To make finding errors easier.
Also, when there are no steps in a scenario, print the information in yellow (so it stands out more and nags for improvement).
Also use a failing sigil for scenarios that have a failed step.
index 08973b8..920caa7 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -145,41 +145,39 @@ Whe a directory is given as the last argument, load all documents inside (recurs
= tagged: math
= source: samples/basic.md:22-34
=
- ⊞ Add 7 and 5 to get 12
- arguments: ["7", "5", "12"]
- source: samples/basic.md:30-30
- ⊞ Divide 10 by 4 to get 2.5
- arguments: ["10", "4", "2.5"]
- source: samples/basic.md:31-31
- ⊞ Subtract 7 from 5 to get -2
- arguments: ["7", "5", "-2"]
- source: samples/basic.md:32-34
+ ⊞ ⊞ ⊞
= ```
=
- Notice it's similar to the output of `tbb show`, but now contains unicode symbols to indicate the results of each step. Each completed scenario has a check mark `✓`.
+ Notice it's similar to the output of `tbb show`, but now contains unicode symbols to indicate the results of each step. Each completed scenario has a check mark `✓`. The number of steps is indicated by the `⊞` sigils.
=
= * The output will contain `text scenario` block
=
= ```text
- ✓ Text
+ x Text
= tagged: strings work-in-progress
= source: samples/basic.md:35-91
=
+
= ⊞ The word blocks has 6 characters
= arguments: ["blocks", "6"]
= source: samples/basic.md:45-45
+
= ⊞ There are 3 properties in the following JSON
= arguments: ["3"], code blocks: 1
= source: samples/basic.md:46-57
+
= ⊞ There are 3 rs in the word strawberry
= arguments: ["3", "r", "strawberry"]
= source: samples/basic.md:58-58
+
= ⊞ The following table maps words to their lengths
= code blocks: 1, tables: 1
= source: samples/basic.md:59-84
+
= ⊞ The reverse of abc is cba
= arguments: ["abc", "cba"]
= source: samples/basic.md:85-85
+
= ⊠ The reverse of CIA is KGB
= arguments: ["CIA", "KGB"]
= source: samples/basic.md:86-89
@@ -194,7 +192,7 @@ Whe a directory is given as the last argument, load all documents inside (recurs
= source: samples/basic.md:90-91
= ```
=
- 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 `□`.
+ If some steps fail, the output will be expanded. Successful steps have a squared plus `⊞` sigil . 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 `□`.
=
= * The output will contain `geometry scenario` block
=index 0205116..30dcf1a 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -257,6 +257,7 @@ pub struct StepReport<'a> {
= status: StepStatus,
=}
=
+#[derive(Debug, Eq, PartialEq)]
=pub enum StepStatus {
= Ok,
= Failed {
@@ -283,8 +284,15 @@ impl Display for EvaluationReport<'_> {
= steps,
= } in scenarios.iter()
= {
+ let all_ok = steps.iter().all(|step| step.status == StepStatus::Ok);
= let sigil = match status {
- ScenarioStatus::Done => "✓".to_string().bold(), // TODO: Use different icon depending on steps status
+ ScenarioStatus::Done => {
+ if all_ok {
+ "✓".to_string().bold().green()
+ } else {
+ "x".to_string().bold().red()
+ }
+ }
= ScenarioStatus::Pending => "?".to_string().bold().dimmed(),
= ScenarioStatus::FailedToRun { .. } => "x".to_string().bold().red(),
= };
@@ -309,9 +317,12 @@ impl Display for EvaluationReport<'_> {
= "{}",
= "There are no steps to execute in this scenario."
= .indent(2)
- .dimmed()
+ .yellow()
= )?
- }
+ } else if all_ok {
+ // Indentation for condensed output
+ write!(f, " ")?;
+ };
=
= for StepReport { step, status } in steps.iter() {
= let sigil = match status {
@@ -319,9 +330,16 @@ impl Display for EvaluationReport<'_> {
= StepStatus::Failed { .. } => "⊠".to_string().bold().red(),
= StepStatus::NotEvaluated => "□".to_string().bold(),
= };
+
+ // Condense output if everything is ok
+ if all_ok {
+ write!(f, "{sigil} ")?;
+ continue;
+ }
+
= writeln!(
= f,
- "{indentation}{sigil} {}",
+ "\n{indentation}{sigil} {}",
= step.to_string().indent(4).trim(),
= indentation = "".indent(2),
= )?;
@@ -329,10 +347,11 @@ impl Display for EvaluationReport<'_> {
= if let StepStatus::Failed { reason, hint } = status {
= writeln!(f, "\n{}\n", reason.indent(4).red())?;
= if let Some(hint) = hint {
- writeln!(f, "{}\n", hint.as_str().indent(4))?;
+ writeln!(f, "{}", hint.as_str().indent(4))?;
= }
= }
= }
+ writeln!(f, "")?;
= }
= writeln!(f, "")?;
= }Bump minor version to 0.9
To celebrate source information feature, condensed output and renaming "list" to "show".
index 09ff789..0297ef9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.8.0"
+version = "0.9.0"
=dependencies = [
= "anyhow",
= "clap",index 768e846..e4b9e37 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.8.0"
+version = "0.9.0"
=edition = "2024"
=
=[dependencies]Update the roadmap
index 7762c58..d4be474 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -53,15 +53,15 @@ We use this document mostly to keep track of our progress and a scratchpad for i
= - [ ] More instructive error messages
= - [x] Indent multiline error messages
= - [ ] Collapse ommited steps (`□□□□□□ following 6 steps skipped`)
- - [ ] Collapse filtered out suites
- - [ ] Collapse filtered out scenarios
- - [ ] Mark scenarios without steps
+ - [x] Collapse filtered out suites
+ - [x] Collapse filtered out scenarios
+ - [x] Mark scenarios without steps
=- [ ] Pass more step data to interpreters
= - [x] Code blocks
= - [x] Tables
= - [x] Lists
= - [ ] Definition lists
- - [ ] Original markdown fragment
+ - [x] Original markdown fragment (path and lines range)
= - [ ] Block quotes
=- [x] Nix package (from Flake)
=- [x] Use for evaluating Jewiet's Form to Mail specification