Commits: 1
Implement Text observation
It's similar to Snippet, but simpler - doesn't have a language and meta, just content. In the report it's rendered as Markdown block quote.
To make this rendering work I implemented the prefix_with method in
the Indentable trait. Under the hood indent is now using this new
method to prefix it's subject with spaces.
index c30568f..b5b548c 100644
--- a/samples/basic.md
+++ b/samples/basic.md
@@ -56,14 +56,10 @@ This
= ```
=
= * There are `3` `r`s in the word `strawberry`
- * The following table maps words to their lengths
+ * The following tables map words to their lengths
+
+ This step demonstrates the use of tables. They can be useful to run tests on multiple samples.
=
- | word | length |
- |--------|--------|
- | cat | 3 |
- | stork | 5 |
- | snake | 5 |
-
= | word | length |
= |--------|--------|
= | cat | 3 |
@@ -71,7 +67,7 @@ This
= | rabbit | 6 |
= | snake | 5 |
= | minx | 4 |
-
+
= Any table within a step will be passed to the interpreter as a 2d array, like so:
=
= ``` json
@@ -86,12 +82,19 @@ This
= ```
=
= Note that heading row is not treated in any special way, and all cells are strings. It's up to an interpreter to deal with it.
-
+
+ Just like with code blocks, you can have multiple tables.
+
+ | word | length |
+ |---------|--------|
+ | rust | 4 |
+ | clojure | 7 |
+ | elm | 3 |
=
= * 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.
+ This step is intentionally wrong to allow demonstrate that TBB will not proceed with following steps. The report from a failing scenario should also display more diagnostic information from each step.
=
= * There are `2` `o`s in the word `boost`
=
@@ -101,4 +104,4 @@ This
=tags: [math]
=```
=
-There are no steps in this scenario. It's ok. Sometimes it's easy to start by describing the behavior in prose, and write formal steps to verify it later. A scenario like this will be listed, but won't be evaluated.
+There are no steps in this scenario. It's ok. Sometimes it's easy to start by describing the behavior in prose, and write formal steps to verify it later. A scenario like this will be listed, but won't be evaluated and won't have any effect on the result.index 01fc945..75f73ef 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -4,7 +4,7 @@ import json
=import unittest
=
=import spec.tbb as tbb
-from spec.tbb import step, log, send_snippet
+from spec.tbb import step, log, send_snippet, send_text
=
=
=# Nice assertions with helpful error messages
@@ -12,19 +12,19 @@ tester = unittest.TestCase()
=
=@step("Add {0} and {1} to get {2}")
=def add_and_verify(a: float, b: float, expected: float, **kwargs):
- send_snippet("text", f"{ a } + { b } = { expected }?")
+ send_text(f"Is { a } + { b } = { expected }? I think it's {a + b}")
=
= tester.assertEqual(expected, a + b)
=
=@step("Divide {0} by {1} to get {2}")
=def divide_and_verify(a: float, b: float, expected: float, **kwargs):
- log.debug(f"{ a } / { b } = { expected }?")
+ send_text(f"{ a } / { b } = { expected }?")
=
= tester.assertAlmostEqual (expected, a / b)
=
=@step("The word {0} has {1} characters")
=def verify_characters_count(word, expected_length: int, **kwargs):
- log.debug(f"Is '{word}' of length {expected_length}?")
+ send_text(f"Is '{word}' of length {expected_length}?")
= actual_length = len(word)
=
= tester.assertEqual(expected_length, actual_length)
@@ -32,12 +32,13 @@ def verify_characters_count(word, expected_length: int, **kwargs):
=@step("There are {0} properties in the following JSON")
=def count_json_properties(expected_count: int, **kwargs):
= code = kwargs['code_blocks'][0]['value']
- log.debug(f"Are there {expected_count} properties in the following?\n{code}")
+ send_text(f"Are there {expected_count} properties in the following?\n{code}")
= data = json.loads(code)
= tester.assertEqual(expected_count, len(data))
=
=@step("Subtract {0} from {1} to get {2}")
=def step_implementation_04(a: float, b: float, expected: float, **kwargs):
+ send_text(f"Is { a } - { b } = { expected }? I think it's {a - b}")
= tester.assertAlmostEqual(expected, b - a)
=
=@step("There are {0} {1}s in the word {2}")
@@ -48,18 +49,20 @@ def step_implementation_05(count: int, letter: str, word: str, **kwargs):
=@step("The reverse of {0} is {1}")
=def step_implementation_06(word: str, reverse: str, **kwargs):
= actual_reverse = word[::-1]
+ send_text(f"Is '{reverse}' the reverse of '{word}'? I think the reverse is '{actual_reverse}'.")
= tester.assertEqual(reverse, actual_reverse)
=
-@step("The following table maps words to their lengths")
+@step("The following tables map words to their lengths")
=def step_implementation_07(**kwargs):
= for table in kwargs["tables"]:
- send_snippet("text", f"Received a table with {len(table)} x {len(table[0])} cells")
+ send_text(f"Received a table with {len(table)} x {len(table[0])} cells")
+ send_snippet("json", json.dumps(table, indent=2))
=
= # Skip the first row - it's a heading
= for [word, length] in table[1:]:
- send_snippet("text", f"Is '{word}' {length} long?")
= actual_length = len(word)
+ send_text(f"Is '{word}' {length} characters long? I think it's {actual_length}.")
= tester.assertEqual(actual_length, int(length), f"the length of {word=}")
=
-
+# After you are done with the setup, call the `ready` function.
=tbb.ready()index 76eb920..74e1b01 100644
--- a/spec/tbb.py
+++ b/spec/tbb.py
@@ -185,7 +185,17 @@ def ready():
= break
=
=
+def send_text(content):
+ """ Send a fragment of text to be displayed in a TBB report
+ """
+ send({
+ "type": "Text",
+ "content": content,
+ })
+
=def send_snippet(language, content, meta = ""):
+ """ Send a snippet of code to be displayed in a TBB report
+ """
= send({
= "type": "Snippet",
= "language": language,index d2c897c..7b4016f 100644
--- a/src/indentable.rs
+++ b/src/indentable.rs
@@ -1,15 +1,19 @@
=pub trait Indentable {
= fn indent(self, width: usize) -> String;
+ fn prefix_with(self, prefix: &str) -> String;
=}
=
=impl Indentable for &str {
= fn indent(self, width: usize) -> String {
= let spaces = " ".repeat(width);
+ self.prefix_with(&spaces)
+ }
+ fn prefix_with(self, prefix: &str) -> String {
= self.replace("\r\n", "\n")
= .split("\n")
- .map(|line| spaces.clone() + line)
+ .map(|line| format!("{prefix}{line}"))
= .reduce(|accu, line| accu + "\n" + &line)
- .unwrap_or(spaces)
+ .unwrap_or(prefix.to_string())
= }
=}
=
@@ -17,105 +21,205 @@ impl Indentable for &str {
=mod tests {
= use super::*;
=
- #[test]
- fn test_single_line() {
- let input = "hello";
- let expected = " hello";
- // 4 spaces
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_single_line_2() {
- let input = "hello";
- let expected = " hello";
- // 2 spaces
-
- assert_eq!(input.indent(2), expected);
- }
-
- #[test]
- fn test_multiple_lines() {
- let input = "hello\nworld";
- let expected = " hello\n world";
-
- assert_eq!(input.indent(2), expected);
+ mod prefix_with {
+ use super::*;
+
+ #[test]
+ fn test_single_line() {
+ let input = "hello";
+ let prefix = "*";
+ let expected = "*hello";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_single_line_with_whitespace() {
+ let input = "hello";
+ let prefix = " ! ";
+ let expected = " ! hello";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_multiple_lines() {
+ let input = "hello\nworld";
+ let prefix = "> ";
+ let expected = "> hello\n> world";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_multiple_lines_with_cariege_return() {
+ let input = "hello\r\nworld\r\nand\r\nmoon";
+ let prefix = "# ";
+ let expected = "# hello\n# world\n# and\n# moon";
+ // \r\n will be replaced with \n
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_empty_string() {
+ let input = "";
+ let prefix = "!!! ";
+ let expected = "!!! ";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_leading_line() {
+ let input = "\nhello";
+ let prefix = "Yo ";
+ let expected = "Yo \nYo hello";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_trailing_newline() {
+ let input = "hello\n";
+ let prefix = "Hey ";
+ let expected = "Hey hello\nHey ";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_multiple_empty_lines() {
+ let input = "\n\n\n";
+ let prefix = "Go!";
+ let expected = "Go!\nGo!\nGo!\nGo!";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_whitespace_in_content() {
+ let input = " leading spaces \n trailing spaces ";
+ let prefix = "|";
+ let expected = "| leading spaces \n| trailing spaces ";
+ // leading and trailing spaces will be preserved, so:
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
+
+ #[test]
+ fn test_no_newline_in_string() {
+ let input = "abc";
+ let prefix = "+";
+ let expected = "+abc";
+
+ assert_eq!(input.prefix_with(prefix), expected);
+ }
= }
=
- #[test]
- fn test_multiple_lines_with_cariege_return() {
- let input = "hello\r\nworld\r\nand\r\nmoon";
- let expected = " hello\n world\n and\n moon";
- // \r\n will be replaced with \n
-
- assert_eq!(input.indent(2), expected);
- }
-
- #[test]
- fn test_empty_string() {
- let input = "";
- let expected = " ";
- // 4 spaces
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_leading_line() {
- let input = "\nhello";
- let expected = " \n hello";
- // 4 spaces
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_trailing_newline() {
- let input = "hello\n";
- let expected = " hello\n ";
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_multiple_empty_lines() {
- let input = "\n\n\n";
- let expected = " \n \n \n ";
- // four lines each containing four spaces
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_whitespace_in_content() {
- let input = " leading spaces \n trailing spaces ";
- let expected = " leading spaces \n trailing spaces ";
- // leading and trailing spaces will be preserved, so:
- // line 1: 4 (indentation) + 2 (original) leading spaces
- // 2 trailing spaces untouched
- // line 2: 4 + 3 leading spaces
- // 1 trailing space
- //
-
- assert_eq!(input.indent(4), expected);
- }
-
- #[test]
- fn test_large_indent() {
- let input = "a";
- let expected = " a";
- // 40 spaces
-
- assert_eq!(input.indent(40), expected);
- }
-
- #[test]
- fn test_no_newline_in_string() {
- let input = "abc";
- let expected = " abc";
-
- assert_eq!(input.indent(4), expected);
+ mod indent {
+ use super::*;
+
+ #[test]
+ fn test_single_line() {
+ let input = "hello";
+ let expected = " hello";
+ // 4 spaces
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_single_line_2() {
+ let input = "hello";
+ let expected = " hello";
+ // 2 spaces
+
+ assert_eq!(input.indent(2), expected);
+ }
+
+ #[test]
+ fn test_multiple_lines() {
+ let input = "hello\nworld";
+ let expected = " hello\n world";
+
+ assert_eq!(input.indent(2), expected);
+ }
+
+ #[test]
+ fn test_multiple_lines_with_cariege_return() {
+ let input = "hello\r\nworld\r\nand\r\nmoon";
+ let expected = " hello\n world\n and\n moon";
+ // \r\n will be replaced with \n
+
+ assert_eq!(input.indent(2), expected);
+ }
+
+ #[test]
+ fn test_empty_string() {
+ let input = "";
+ let expected = " ";
+ // 4 spaces
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_leading_line() {
+ let input = "\nhello";
+ let expected = " \n hello";
+ // 4 spaces
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_trailing_newline() {
+ let input = "hello\n";
+ let expected = " hello\n ";
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_multiple_empty_lines() {
+ let input = "\n\n\n";
+ let expected = " \n \n \n ";
+ // four lines each containing four spaces
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_whitespace_in_content() {
+ let input = " leading spaces \n trailing spaces ";
+ let expected = " leading spaces \n trailing spaces ";
+ // leading and trailing spaces will be preserved, so:
+ // line 1: 4 (indentation) + 2 (original) leading spaces
+ // 2 trailing spaces untouched
+ // line 2: 4 + 3 leading spaces
+ // 1 trailing space
+ //
+
+ assert_eq!(input.indent(4), expected);
+ }
+
+ #[test]
+ fn test_large_indent() {
+ let input = "a";
+ let expected = " a";
+ // 40 spaces
+
+ assert_eq!(input.indent(40), expected);
+ }
+
+ #[test]
+ fn test_no_newline_in_string() {
+ let input = "abc";
+ let expected = " abc";
+
+ assert_eq!(input.indent(4), expected);
+ }
= }
=}
=index 0906f9c..3ae8227 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -170,6 +170,9 @@ impl<'a> ScenarioReport<'a> {
=
= loop {
= match Self::receive(&mut reader)? {
+ InterpreterMessage::Text { content } => {
+ observations.push(Observation::Text { content })
+ }
= InterpreterMessage::Snippet {
= language,
= meta,
@@ -199,7 +202,7 @@ impl<'a> ScenarioReport<'a> {
= // Maybe this can be configured via front-matter?
= break 'steps_loop;
= }
- unexpected => {
+ unexpected @ InterpreterMessage::InterpreterState { .. } => {
= return Err(anyhow!(
= "unexpected message received from the interpreter: {unexpected:#?}"
= ));
@@ -279,11 +282,19 @@ pub struct StepReport<'a> {
=
=#[derive(Debug, Serialize)]
=pub enum Observation {
+ Text {
+ content: String,
+ },
= Snippet {
= language: String,
= meta: String,
= content: String,
= },
+ // Image,
+ // Audio,
+ // Video,
+ // Attachment,
+ // Link,
=}
=
=impl Display for Observation {
@@ -298,6 +309,9 @@ impl Display for Observation {
= writeln!(f, "{content}")?;
= writeln!(f, "```")
= }
+ Observation::Text { content } => {
+ writeln!(f, "{}", content.prefix_with("> "))
+ }
= }
= }
=}
@@ -474,6 +488,9 @@ pub enum InterpreterMessage {
= InterpreterState {
= ready: bool,
= },
+ Text {
+ content: String,
+ },
= Snippet {
= language: String,
= meta: String,