From 258ee4398bde99009663ff23521cbfd8324312a0 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 20 Jul 2021 15:47:04 +0300 Subject: feat: when more than one bindings match a step, list all of them Sponsored-by: author --- src/bindings.rs | 28 ++++++++++++++++++---------- src/error.rs | 6 ++++-- src/lib.rs | 1 + src/matches.rs | 44 +++++++++++++++++++++++++++++++++++--------- subplot.md | 21 +++++++++++++++++++-- 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/bindings.rs b/src/bindings.rs index d17460f..2e927cd 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -1,4 +1,5 @@ use super::MatchedStep; +use super::MatchedSteps; use super::PartialStep; use super::ScenarioStep; use super::StepKind; @@ -170,7 +171,7 @@ impl Binding { let caps = self.regex.captures(step_text)?; // If there is only one capture, it's the whole string. - let mut m = MatchedStep::new(self.kind(), &self.function, self.cleanup(), &self.types); + let mut m = MatchedStep::new(self, &self.types); if caps.len() == 1 { m.append_part(PartialStep::uncaptured(step_text)); return Some(m); @@ -480,16 +481,23 @@ impl Bindings { /// Find the binding matching a given scenario step, if there is /// exactly one. pub fn find(&self, step: &ScenarioStep) -> Result { - let mut matches = self + let mut matches: Vec = self .bindings() .iter() - .filter_map(|b| b.match_with_step(step)); - match matches.next() { - None => Err(SubplotError::BindingUnknown(step.to_string())), - Some(matched) => match matches.next() { - None => Ok(matched), - Some(_) => Err(SubplotError::BindingNotUnique(step.to_string())), - }, + .filter_map(|b| b.match_with_step(step)) + .collect(); + if matches.len() > 1 { + // Too many matching bindings. + Err(SubplotError::BindingNotUnique( + step.to_string(), + MatchedSteps::new(matches), + )) + } else if let Some(m) = matches.pop() { + // Exactly one matching binding. + Ok(m) + } else { + // No matching bindings. + Err(SubplotError::BindingUnknown(step.to_string())) } } @@ -719,7 +727,7 @@ mod test_bindings { ); assert!(matches!( bindings.find(&step), - Err(SubplotError::BindingNotUnique(_)) + Err(SubplotError::BindingNotUnique(_, _)) )); } diff --git a/src/error.rs b/src/error.rs index eb70e28..532714b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use crate::matches::MatchedSteps; + use std::path::PathBuf; use std::process::Output; @@ -46,8 +48,8 @@ pub enum SubplotError { /// /// THis may be due to bindings being too general, or having unusual /// overlaps in their matching - #[error("more than one binding matches: {0}")] - BindingNotUnique(String), + #[error("more than one binding matches step {0}:\n{1}")] + BindingNotUnique(String, MatchedSteps), /// A binding in the bindings file doesn't specify a known keyword. #[error("binding doesn't specify known keyword: {0}")] diff --git a/src/lib.rs b/src/lib.rs index 960e3ef..e2b1431 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,7 @@ pub use parser::parse_scenario_snippet; mod matches; pub use matches::MatchedScenario; pub use matches::MatchedStep; +pub use matches::MatchedSteps; pub use matches::PartialStep; pub use matches::StepSnippet; diff --git a/src/matches.rs b/src/matches.rs index 472b475..88d2c55 100644 --- a/src/matches.rs +++ b/src/matches.rs @@ -1,3 +1,4 @@ +use crate::Binding; use crate::Result; use crate::Scenario; use crate::StepKind; @@ -34,6 +35,30 @@ impl MatchedScenario { } } +/// A list of matched steps. +#[derive(Debug)] +pub struct MatchedSteps { + matches: Vec, +} + +impl MatchedSteps { + /// Create new set of steps that match a scenario step. + pub fn new(matches: Vec) -> Self { + Self { matches } + } +} + +impl std::fmt::Display for MatchedSteps { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let matches: Vec = self + .matches + .iter() + .map(|m| format!("{}", m.pattern())) + .collect(); + write!(f, "{}", matches.join("\n")) + } +} + /// A matched binding and scenario step, with captured parts. /// /// A MatchedStep is a sequence of partial steps, each representing @@ -41,6 +66,7 @@ impl MatchedScenario { #[derive(Debug, Serialize, Deserialize)] pub struct MatchedStep { kind: StepKind, + pattern: String, text: String, parts: Vec, function: String, @@ -50,18 +76,14 @@ pub struct MatchedStep { impl MatchedStep { /// Return a new empty match. Empty means it has no step parts. - pub fn new( - kind: StepKind, - function: &str, - cleanup: Option<&str>, - types: &HashMap, - ) -> MatchedStep { + pub fn new(binding: &Binding, types: &HashMap) -> MatchedStep { MatchedStep { - kind, + kind: binding.kind(), + pattern: binding.pattern().to_string(), text: "".to_string(), parts: vec![], - function: function.to_string(), - cleanup: cleanup.map(String::from), + function: binding.function().to_string(), + cleanup: binding.cleanup().map(String::from), types: types.clone(), } } @@ -87,6 +109,10 @@ impl MatchedStep { self.parts.iter() } + fn pattern(&self) -> String { + self.pattern.to_string() + } + fn text(&self) -> String { let mut t = String::new(); for part in self.parts() { diff --git a/subplot.md b/subplot.md index 6ddc2c7..07c07de 100644 --- a/subplot.md +++ b/subplot.md @@ -2561,7 +2561,9 @@ then command fails title: Two bindings match template: python bindings: -- badbindings.yaml +- twobindings.yaml +functions: +- a_function.py ... # Broken scenario because step has two possible bindings @@ -2570,12 +2572,27 @@ given a binding ``` ~~~~ +~~~{#twobindings.yaml .file .yaml} +- given: a {xyzzy} + function: a_function +- given: a {plugh} + function: a_function +~~~ + +~~~{#a_function.py .file .python} +def a_function(ctx): + assert 0 +~~~ + ```scenario given file twobindings.md -and file badbindings.yaml +and file twobindings.yaml +given file a_function.py and an installed subplot when I try to run subplot codegen --run twobindings.md -o test.py then command fails +then stderr contains "xyzzy" +then stderr contains "plugh" ``` -- cgit v1.2.1