From f6c164ccc4f9d6400d4ddb5b9993314aea1d0122 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 00:46:49 +0100 Subject: [PATCH 1/6] more options for output and merged structs which was created in flintcli and flint-steel --- Cargo.lock | 10 ++ Cargo.toml | 2 +- src/format.rs | 358 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/results.rs | 166 +++++++++++++---------- 5 files changed, 468 insertions(+), 69 deletions(-) create mode 100644 src/format.rs diff --git a/Cargo.lock b/Cargo.lock index a045686..bcabc13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + [[package]] name = "errno" version = "0.3.14" @@ -41,6 +50,7 @@ name = "flint-core" version = "0.1.0" dependencies = [ "anyhow", + "colored", "serde", "serde_json", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index c512b6c..bed1649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0.100" -# rsjsonnet = "0.4.0" # for jsonnet support +colored = "3" [dev-dependencies] tempfile = "3" diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..cc4039d --- /dev/null +++ b/src/format.rs @@ -0,0 +1,358 @@ +use std::time::Duration; +use crate::results::{AssertFailure, AssertionResult, InfoType, TestResult}; +use std::collections::BTreeMap; +use colored::Colorize; + +/// Extract failures from test results as (test_name, AssertFailure) pairs +fn extract_failures(results: &[TestResult]) -> Vec<(String, AssertFailure)> { + results.iter() + .flat_map(|r| { + r.assertions.iter().filter_map(move |a| { + if let AssertionResult::Failure(f) = a { + Some((r.test_name.clone(), f.clone())) + } else { + None + } + }) + }) + .collect() +} + +/// Convert InfoType to a display string +fn info_type_to_string(info: &InfoType) -> String { + match info { + InfoType::String(s) => s.clone(), + InfoType::Block(b) => format!("{:?}", b), + } +} + +/// Print results as JSON to stdout +pub fn print_json(results: &[TestResult], elapsed: Duration) { + let total = results.len(); + let passed = results.iter().filter(|r| r.success).count(); + let failed = total - passed; + + let failures = extract_failures(results); + let failure_objects: Vec = failures + .iter() + .map(|(name, detail)| { + serde_json::json!({ + "test": name, + "tick": detail.tick, + "expected": info_type_to_string(&detail.expected), + "actual": info_type_to_string(&detail.actual), + "position": detail.position, + }) + }) + .collect(); + + let test_objects: Vec = results + .iter() + .map(|r| { + serde_json::json!({ + "name": r.test_name, + "success": r.success, + "total_ticks": r.total_ticks, + "execution_time_ms": r.execution_time_ms, + }) + }) + .collect(); + + let output = serde_json::json!({ + "summary": { + "total": total, + "passed": passed, + "failed": failed, + "duration_secs": elapsed.as_secs_f64(), + }, + "tests": test_objects, + "failures": failure_objects, + }); + + println!("{}", serde_json::to_string_pretty(&output).unwrap()); +} + +/// Print results in TAP (Test Anything Protocol) version 13 format +pub fn print_tap(results: &[TestResult]) { + println!("TAP version 13"); + println!("1..{}", results.len()); + + let failures = extract_failures(results); + // Build a lookup from test name to failure detail + let failure_map: std::collections::HashMap<&str, &AssertFailure> = failures + .iter() + .map(|(name, detail)| (name.as_str(), detail)) + .collect(); + + for (i, result) in results.iter().enumerate() { + let number = i + 1; + if result.success { + println!("ok {} - {}", number, result.test_name); + } else { + println!("not ok {} - {}", number, result.test_name); + if let Some(detail) = failure_map.get(result.test_name.as_str()) { + println!(" ---"); + println!( + " message: \"expected {}, got {}\"", + info_type_to_string(&detail.expected), info_type_to_string(&detail.actual) + ); + println!( + " at: [{}, {}, {}]", + detail.position[0], detail.position[1], detail.position[2] + ); + println!(" tick: {}", detail.tick); + println!(" ..."); + } + } + } +} + +/// Print results in JUnit XML format +pub fn print_junit( + results: &[TestResult], + elapsed: Duration, +) { + let total = results.len(); + let failed = results.iter().filter(|r| !r.success).count(); + + let failures = extract_failures(results); + let failure_map: std::collections::HashMap<&str, &AssertFailure> = failures + .iter() + .map(|(name, detail)| (name.as_str(), detail)) + .collect(); + + println!(r#""#); + println!( + r#""#, + total, + failed, + elapsed.as_secs_f64() + ); + println!( + r#" "#, + total, + failed, + elapsed.as_secs_f64() + ); + + for result in results { + // Split test name into classname (directory path) and name (leaf) + let (classname, name) = match result.test_name.rsplit_once('/') { + Some((prefix, leaf)) => (prefix, leaf), + None => ("", result.test_name.as_str()), + }; + + let time = result.execution_time_ms as f64 / 1000.0; + + if result.success { + println!( + r#" "#, + xml_escape(classname), + xml_escape(name), + time + ); + } else { + println!( + r#" "#, + xml_escape(classname), + xml_escape(name), + time + ); + if let Some(detail) = failure_map.get(result.test_name.as_str()) { + println!( + r#" "#, + xml_escape(&info_type_to_string(&detail.expected)), + xml_escape(&info_type_to_string(&detail.actual)), + detail.position[0], + detail.position[1], + detail.position[2], + detail.tick + ); + } else { + println!(r#" "#); + } + println!(" "); + } + } + + println!(" "); + println!(""); +} + +fn xml_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} +const SEPARATOR_WIDTH: usize = 60; + +/// Print a separator line +fn print_separator() { + println!("{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); +} + +/// Print verbose test summary (used in -v mode) +pub fn print_test_summary(results: &[TestResult]) { + println!("\n{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); + println!("{}", "Test Summary".cyan().bold()); + print_separator(); + + let total_passed = results.iter().filter(|r| r.success).count(); + let total_failed = results.len() - total_passed; + + for result in results { + let status = if result.success { + "PASS".green().bold() + } else { + "FAIL".red().bold() + }; + println!(" [{}] {}", status, result.test_name); + } + + println!( + "\n{} tests run: {} passed, {} failed\n", + results.len(), + total_passed.to_string().green(), + total_failed.to_string().red() + ); +} + +/// Print concise summary (default mode) +pub fn print_concise_summary( + results: &[TestResult], + elapsed: Duration, +) { + let total = results.len(); + let total_passed = results.iter().filter(|r| r.success).count(); + let total_failed = total - total_passed; + let secs = elapsed.as_secs_f64(); + + println!(); + if total_failed == 0 { + println!( + "{} All {} tests passed ({:.3}s)", + "✓".green().bold(), + format_number(total), + secs + ); + } else { + println!( + "{} of {} tests failed ({:.3}s)", + format_number(total_failed).red().bold(), + format_number(total), + secs + ); + println!(); + let failures = extract_failures(results); + print_failure_tree(&failures); + println!(); + println!( + "{} passed, {} failed", + format_number(total_passed).green(), + format_number(total_failed).red() + ); + } + println!(); +} + +/// Format a number with comma separators (e.g., 1247 -> "1,247") +pub fn format_number(n: usize) -> String { + let s = n.to_string(); + let mut result = String::with_capacity(s.len() + s.len() / 3); + for (i, c) in s.chars().enumerate() { + if i > 0 && (s.len() - i) % 3 == 0 { + result.push(','); + } + result.push(c); + } + result +} + +// ── Failure tree rendering ────────────────────────────────── + +/// A tree node for grouping failures by path segments +pub struct TreeNode { + children: BTreeMap, + failure: Option, +} + +impl TreeNode { + fn new() -> Self { + Self { + children: BTreeMap::new(), + failure: None, + } + } + + fn insert(&mut self, segments: &[&str], detail: AssertFailure) { + if segments.is_empty() { + self.failure = Some(detail); + return; + } + let child = self + .children + .entry(segments[0].to_string()) + .or_insert_with(TreeNode::new); + if segments.len() == 1 { + child.failure = Some(detail); + } else { + child.insert(&segments[1..], detail); + } + } +} + +/// Print the failure tree +fn print_failure_tree(failures: &[(String, AssertFailure)]) { + let mut root = TreeNode::new(); + + for (name, detail) in failures { + let segments: Vec<&str> = name.split('/').collect(); + root.insert(&segments, detail.clone()); + } + + // Render each top-level child + let keys: Vec<_> = root.children.keys().cloned().collect(); + for (i, key) in keys.iter().enumerate() { + let is_last = i == keys.len() - 1; + let child = root.children.get(key).unwrap(); + render_tree_node(key, child, "", is_last); + } +} + +fn render_tree_node(name: &str, node: &TreeNode, prefix: &str, is_last: bool) { + let connector = if is_last { "└── " } else { "├── " }; + let child_prefix = if is_last { " " } else { "│ " }; + + if node.children.is_empty() { + // Leaf node: print name with failure detail + if let Some(ref detail) = node.failure { + println!("{}{}{}", prefix, connector, name); + let detail_connector = if is_last { " " } else { "│ " }; + println!( + "{}{}└─ t{}: expected {}, got {} @ ({},{},{})", + prefix, + detail_connector, + detail.tick, + info_type_to_string(&detail.expected).green(), + info_type_to_string(&detail.actual).red(), + detail.position[0], + detail.position[1], + detail.position[2] + ); + } else { + println!("{}{}{}", prefix, connector, name); + } + } else { + // Branch node + println!("{}{}{}", prefix, connector, name); + let new_prefix = format!("{}{}", prefix, child_prefix); + let keys: Vec<_> = node.children.keys().cloned().collect(); + for (i, key) in keys.iter().enumerate() { + let child_is_last = i == keys.len() - 1; + let child = node.children.get(key).unwrap(); + render_tree_node(key, child, &new_prefix, child_is_last); + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1cd5fac..e3d475c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod spatial; pub mod test_spec; pub mod timeline; pub mod utils; +pub mod format; diff --git a/src/results.rs b/src/results.rs index 5bed94c..e5d6488 100644 --- a/src/results.rs +++ b/src/results.rs @@ -1,55 +1,77 @@ +use crate::results::AssertionResult::Failure; +use crate::test_spec::Block; use serde::{Deserialize, Serialize}; -/// Result of executing a single assertion or action +/// Outcome of executing a single action +pub enum ActionOutcome { + /// Non-assertion action completed (place, fill, remove) + Action, + /// Assertion passed + AssertPassed, + /// Assertion failed with details + AssertFailed(AssertFailure), +} + #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AssertionResult { - /// The tick at which this assertion was executed - pub tick: u32, +pub enum InfoType { + String(String), + Block(Block), +} - /// Whether the assertion succeeded - pub success: bool, +impl InfoType { + pub fn get_string(&self) -> Option { + match self { + InfoType::String(s) => Some(s.clone()), + InfoType::Block(_) => None, + } + } +} - /// Type of action (e.g., "Assert", "AssertState") - pub action_type: String, +/// Result of executing a single assertion or action +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AssertionResult { + Success(u32), + Failure(AssertFailure), +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssertFailure { + /// The tick at which this assertion was executed + pub tick: u32, /// Error message if the assertion failed - pub error_message: Option, - + pub error_message: String, /// Position involved in the assertion, if applicable - pub position: Option<[i32; 3]>, - + pub position: [i32; 3], /// Time taken to execute this assertion in milliseconds pub execution_time_ms: Option, + /// What as expected + pub expected: InfoType, + /// What was found + pub actual: InfoType, } -impl AssertionResult { - /// Create a successful assertion result - pub fn success(tick: u32, action_type: impl Into) -> Self { - Self { - tick, - success: true, - action_type: action_type.into(), - error_message: None, - position: None, - execution_time_ms: None, - } - } - +impl AssertFailure { /// Create a failed assertion result - pub fn failure(tick: u32, action_type: impl Into, error: impl Into) -> Self { + pub fn new( + tick: u32, + error: impl Into, + position: [i32; 3], + expected: InfoType, + actual: InfoType, + ) -> AssertFailure { Self { tick, - success: false, - action_type: action_type.into(), - error_message: Some(error.into()), - position: None, + error_message: error.into(), + position, execution_time_ms: None, + expected, + actual, } } /// Add position information to the assertion result pub fn with_position(mut self, pos: [i32; 3]) -> Self { - self.position = Some(pos); + self.position = pos; self } @@ -58,6 +80,12 @@ impl AssertionResult { self.execution_time_ms = Some(ms); self } + /// adds the expected and the actual found data + pub fn with_expected_actual(mut self, expected: InfoType, actual: InfoType) -> Self { + self.actual = actual; + self.expected = expected; + self + } } /// Result of executing a complete test @@ -101,11 +129,8 @@ impl TestResult { /// Add an assertion result to this test result pub fn add_assertion(&mut self, assertion: AssertionResult) { - if !assertion.success { + if let Failure(_) = assertion { self.success = false; - if self.failure_reason.is_none() { - self.failure_reason = assertion.error_message.clone(); - } } self.assertions.push(assertion); } @@ -137,12 +162,18 @@ impl TestResult { /// Get the number of passed assertions pub fn passed_count(&self) -> usize { - self.assertions.iter().filter(|a| a.success).count() + self.assertions + .iter() + .filter(|a| matches!(a, AssertionResult::Success(_))) + .count() } /// Get the number of failed assertions pub fn failed_count(&self) -> usize { - self.assertions.iter().filter(|a| !a.success).count() + self.assertions + .iter() + .filter(|a| !matches!(a, AssertionResult::Success(_))) + .count() } /// Get the total number of assertions @@ -216,30 +247,37 @@ impl TestSummary { mod tests { use super::*; + fn make_success(tick: u32) -> AssertionResult { + AssertionResult::Success(tick) + } + + fn make_failure(tick: u32, error: &str, position: [i32; 3]) -> AssertionResult { + Failure(AssertFailure::new( + tick, + error, + position, + InfoType::String("expected".to_string()), + InfoType::String("actual".to_string()), + )) + } + #[test] fn test_assertion_result_success() { - let result = AssertionResult::success(5, "Assert") - .with_position([1, 2, 3]) - .with_timing(100); - - assert!(result.success); - assert_eq!(result.tick, 5); - assert_eq!(result.action_type, "Assert"); - assert_eq!(result.position, Some([1, 2, 3])); - assert_eq!(result.execution_time_ms, Some(100)); - assert!(result.error_message.is_none()); + let result = make_success(5); + assert!(matches!(result, AssertionResult::Success(5))); } #[test] fn test_assertion_result_failure() { - let result = - AssertionResult::failure(10, "AssertState", "Block mismatch").with_position([5, 6, 7]); + let result = make_failure(10, "Block mismatch", [5, 6, 7]); - assert!(!result.success); - assert_eq!(result.tick, 10); - assert_eq!(result.action_type, "AssertState"); - assert_eq!(result.error_message, Some("Block mismatch".to_string())); - assert_eq!(result.position, Some([5, 6, 7])); + if let Failure(f) = result { + assert_eq!(f.tick, 10); + assert_eq!(f.error_message, "Block mismatch"); + assert_eq!(f.position, [5, 6, 7]); + } else { + panic!("Expected Failure variant"); + } } #[test] @@ -249,8 +287,8 @@ mod tests { .with_execution_time(5000) .with_offset([0, 0, 0]); - result.add_assertion(AssertionResult::success(5, "Assert")); - result.add_assertion(AssertionResult::success(10, "AssertState")); + result.add_assertion(make_success(5)); + result.add_assertion(make_success(10)); assert!(result.success); assert_eq!(result.passed_count(), 2); @@ -263,29 +301,21 @@ mod tests { fn test_test_result_with_failure() { let mut result = TestResult::new("test2"); - result.add_assertion(AssertionResult::success(5, "Assert")); - result.add_assertion(AssertionResult::failure( - 10, - "Assert", - "Expected stone, got dirt", - )); - result.add_assertion(AssertionResult::success(15, "AssertState")); + result.add_assertion(make_success(5)); + result.add_assertion(make_failure(10, "Expected stone, got dirt", [0, 0, 0])); + result.add_assertion(make_success(15)); assert!(!result.success); assert_eq!(result.passed_count(), 2); assert_eq!(result.failed_count(), 1); assert_eq!(result.total_assertions(), 3); - assert_eq!( - result.failure_reason, - Some("Expected stone, got dirt".to_string()) - ); } #[test] fn test_test_summary() { let result1 = TestResult::new("test1").with_execution_time(1000); let mut result2 = TestResult::new("test2").with_execution_time(2000); - result2.add_assertion(AssertionResult::failure(5, "Assert", "Failed")); + result2.add_assertion(make_failure(5, "Failed", [0, 0, 0])); let summary = TestSummary::from_results(vec![result1, result2]); From 6e9e2e96954f1302e906f02707857023b3c9fc9a Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 00:48:39 +0100 Subject: [PATCH 2/6] cargo fmt --- src/format.rs | 22 +++++++++------------- src/lib.rs | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/format.rs b/src/format.rs index cc4039d..d3116c8 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,11 +1,12 @@ -use std::time::Duration; use crate::results::{AssertFailure, AssertionResult, InfoType, TestResult}; -use std::collections::BTreeMap; use colored::Colorize; +use std::collections::BTreeMap; +use std::time::Duration; /// Extract failures from test results as (test_name, AssertFailure) pairs fn extract_failures(results: &[TestResult]) -> Vec<(String, AssertFailure)> { - results.iter() + results + .iter() .flat_map(|r| { r.assertions.iter().filter_map(move |a| { if let AssertionResult::Failure(f) = a { @@ -94,7 +95,8 @@ pub fn print_tap(results: &[TestResult]) { println!(" ---"); println!( " message: \"expected {}, got {}\"", - info_type_to_string(&detail.expected), info_type_to_string(&detail.actual) + info_type_to_string(&detail.expected), + info_type_to_string(&detail.actual) ); println!( " at: [{}, {}, {}]", @@ -108,10 +110,7 @@ pub fn print_tap(results: &[TestResult]) { } /// Print results in JUnit XML format -pub fn print_junit( - results: &[TestResult], - elapsed: Duration, -) { +pub fn print_junit(results: &[TestResult], elapsed: Duration) { let total = results.len(); let failed = results.iter().filter(|r| !r.success).count(); @@ -220,10 +219,7 @@ pub fn print_test_summary(results: &[TestResult]) { } /// Print concise summary (default mode) -pub fn print_concise_summary( - results: &[TestResult], - elapsed: Duration, -) { +pub fn print_concise_summary(results: &[TestResult], elapsed: Duration) { let total = results.len(); let total_passed = results.iter().filter(|r| r.success).count(); let total_failed = total - total_passed; @@ -355,4 +351,4 @@ fn render_tree_node(name: &str, node: &TreeNode, prefix: &str, is_last: bool) { render_tree_node(key, child, &new_prefix, child_is_last); } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index e3d475c..59db326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod format; pub mod index; pub mod loader; pub mod results; @@ -5,4 +6,3 @@ pub mod spatial; pub mod test_spec; pub mod timeline; pub mod utils; -pub mod format; From f588a63ccc791996dfeabcbe16f52d15ca965f78 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 00:56:48 +0100 Subject: [PATCH 3/6] fixed tests??? --- src/results.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/results.rs b/src/results.rs index e5d6488..9d98e8f 100644 --- a/src/results.rs +++ b/src/results.rs @@ -26,6 +26,14 @@ impl InfoType { } } } +impl Into for InfoType { + fn into(self) -> String { + match self { + InfoType::String(s) => s.clone(), + InfoType::Block(b) => b.to_command(), + } + } +} /// Result of executing a single assertion or action #[derive(Debug, Clone, Serialize, Deserialize)] From 0b125fd541ee0323c0cad9ac23460b2426ffcd32 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 01:02:36 +0100 Subject: [PATCH 4/6] removed constant --- src/format.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/format.rs b/src/format.rs index d3116c8..72974bc 100644 --- a/src/format.rs +++ b/src/format.rs @@ -185,18 +185,17 @@ fn xml_escape(s: &str) -> String { .replace('"', """) .replace('\'', "'") } -const SEPARATOR_WIDTH: usize = 60; /// Print a separator line -fn print_separator() { - println!("{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); +fn print_separator(separator_width: usize) { + println!("{}", "═".repeat(separator_width).dimmed()); } /// Print verbose test summary (used in -v mode) -pub fn print_test_summary(results: &[TestResult]) { - println!("\n{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); +pub fn print_test_summary(results: &[TestResult], separator_width: usize) { + println!("\n{}", "═".repeat(separator_width).dimmed()); println!("{}", "Test Summary".cyan().bold()); - print_separator(); + print_separator(separator_width); let total_passed = results.iter().filter(|r| r.success).count(); let total_failed = results.len() - total_passed; From 80059e0e4324e34a20f90efbfca5892efa176d03 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 01:05:21 +0100 Subject: [PATCH 5/6] clippy and fmt --- src/format.rs | 2 +- src/results.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/format.rs b/src/format.rs index 72974bc..4ec4c13 100644 --- a/src/format.rs +++ b/src/format.rs @@ -257,7 +257,7 @@ pub fn format_number(n: usize) -> String { let s = n.to_string(); let mut result = String::with_capacity(s.len() + s.len() / 3); for (i, c) in s.chars().enumerate() { - if i > 0 && (s.len() - i) % 3 == 0 { + if i > 0 && (s.len() - i).is_multiple_of(3) { result.push(','); } result.push(c); diff --git a/src/results.rs b/src/results.rs index 9d98e8f..be94a88 100644 --- a/src/results.rs +++ b/src/results.rs @@ -26,9 +26,9 @@ impl InfoType { } } } -impl Into for InfoType { - fn into(self) -> String { - match self { +impl From for String { + fn from(val: InfoType) -> String { + match val { InfoType::String(s) => s.clone(), InfoType::Block(b) => b.to_command(), } From babb2923c33f159b08dc3f7a3f8085676a80b920 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Thu, 29 Jan 2026 01:08:55 +0100 Subject: [PATCH 6/6] do it better now --- src/results.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/results.rs b/src/results.rs index be94a88..ce9a0fd 100644 --- a/src/results.rs +++ b/src/results.rs @@ -34,6 +34,14 @@ impl From for String { } } } +impl From<&InfoType> for String { + fn from(val: &InfoType) -> String { + match val { + InfoType::String(s) => s.clone(), + InfoType::Block(b) => b.to_command(), + } + } +} /// Result of executing a single assertion or action #[derive(Debug, Clone, Serialize, Deserialize)]