From f70647d4f57c2adf93daec84c2f510241b60f70d Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 12:06:55 +0100 Subject: feat: Scenario keyword caseless matching Signed-off-by: Daniel Silverstone --- src/steps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steps.rs b/src/steps.rs index 1cbdfe5..c2e8374 100644 --- a/src/steps.rs +++ b/src/steps.rs @@ -55,7 +55,7 @@ impl ScenarioStep { _ => return Err(SubplotError::NoStepKeyword(text.to_string())), }; - let kind = match keyword { + let kind = match keyword.to_ascii_lowercase().as_str() { "given" => StepKind::Given, "when" => StepKind::When, "then" => StepKind::Then, -- cgit v1.2.1 From 37565e21534ec83eda0088d55d1680f7e71ab6f1 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 12:44:59 +0100 Subject: feat: Ensure keys in bindings files are case insensitive Signed-off-by: Daniel Silverstone --- Cargo.lock | 13 +++++++++++++ Cargo.toml | 1 + src/bindings.rs | 14 +++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dabe73..3f75316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -556,6 +556,17 @@ dependencies = [ "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde-aux" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serde_derive" version = "1.0.104" @@ -645,6 +656,7 @@ dependencies = [ "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "roadmap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde-aux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -958,6 +970,7 @@ dependencies = [ "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +"checksum serde-aux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae50f53d4b01e854319c1f5b854cd59471f054ea7e554988850d3f36ca1dc852" "checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" "checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" "checksum serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)" = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35" diff --git a/Cargo.toml b/Cargo.toml index 333217f..1b88a76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ regex = "1" serde_yaml = "0.8.11" serde_json = "1.0" serde = { version = "1.0.101", features = ["derive"] } +serde-aux = "0.6.1" chrono = "0.4" thiserror = "1" anyhow = "1" diff --git a/src/bindings.rs b/src/bindings.rs index 40ef0b4..84b331b 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -5,6 +5,7 @@ use super::StepKind; use crate::{Result, SubplotError}; use serde::Deserialize; +use serde_aux::prelude::*; use std::fs::File; use std::io::Read; @@ -257,6 +258,13 @@ struct ParsedBinding { regex: Option, } +#[derive(Debug, Deserialize)] +#[serde(transparent)] +struct ParsedBindingWrapper { + #[serde(deserialize_with = "deserialize_struct_case_insensitive")] + binding: ParsedBinding, +} + impl Bindings { /// Create a new, empty set of bindings. pub fn new() -> Bindings { @@ -280,9 +288,9 @@ impl Bindings { /// Add bindings from a YAML string pub fn add_from_yaml(&mut self, yaml: &str) -> Result<()> { - let bindings: Vec = serde_yaml::from_str(yaml)?; - for b in bindings { - self.add(from_hashmap(&b)?); + let bindings: Vec = serde_yaml::from_str(yaml)?; + for wrapper in bindings { + self.add(from_hashmap(&wrapper.binding)?); } Ok(()) } -- cgit v1.2.1 From b0040f567142b5d53f675f4d67a21928d628e27e Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 12:46:15 +0100 Subject: test: Ensure case insensitive bindings work Signed-off-by: Daniel Silverstone --- src/bindings.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bindings.rs b/src/bindings.rs index 84b331b..617d725 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -410,11 +410,11 @@ mod test_bindings { #[test] fn adds_from_yaml() { let yaml = " -- given: I am Tomjon +- GIVEN: I am Tomjon function: set_name - when: I declare myself king - function: declare_king -- then: there is applause + Function: declare_king +- tHEn: there is applause function: check_for_applause "; let mut bindings = Bindings::new(); @@ -429,9 +429,9 @@ mod test_bindings { #[test] fn add_from_yaml_notices_multiple_keywords() { let yaml = " -- given: I am Tomjon - when: I am indeed Tomjon - function: set_name +- Given: I am Tomjon + wheN: I am indeed Tomjon + FUNCTION: set_name "; match Bindings::new().add_from_yaml(&yaml) { Ok(_) => unreachable!(), -- cgit v1.2.1 From a80f1243f5b65b8b6876e0c2cd81134155d7af65 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 12:47:13 +0100 Subject: test: Ensure that scenario keywords match insensitively Signed-off-by: Daniel Silverstone --- src/steps.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/steps.rs b/src/steps.rs index c2e8374..9af526c 100644 --- a/src/steps.rs +++ b/src/steps.rs @@ -116,7 +116,7 @@ mod test { #[test] fn parses_given() { - let step = ScenarioStep::new_from_str("given I am Tomjon", None).unwrap(); + let step = ScenarioStep::new_from_str("GIVEN I am Tomjon", None).unwrap(); assert_eq!(step.kind(), StepKind::Given); assert_eq!(step.text(), "I am Tomjon"); } @@ -137,7 +137,7 @@ mod test { #[test] fn parses_then() { - let step = ScenarioStep::new_from_str("then everyone accepts it", None).unwrap(); + let step = ScenarioStep::new_from_str("thEN everyone accepts it", None).unwrap(); assert_eq!(step.kind(), StepKind::Then); assert_eq!(step.text(), "everyone accepts it"); } -- cgit v1.2.1 From 7572bc1ad516b9686427558e98a069738bace73d Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 14:17:12 +0100 Subject: feat: Case insensitive matching by default Signed-off-by: Daniel Silverstone --- src/bindings.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 20 deletions(-) diff --git a/src/bindings.rs b/src/bindings.rs index 617d725..9d80394 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -11,7 +11,7 @@ use std::fs::File; use std::io::Read; use std::path::Path; -use regex::{escape, Regex}; +use regex::{escape, Regex, RegexBuilder}; /// A binding of a scenario step to its implementation. /// @@ -34,11 +34,15 @@ impl Binding { pattern: &str, function: &str, cleanup: Option<&str>, + case_sensitive: bool, ) -> Result { + let regex = RegexBuilder::new(&format!("^{}$", pattern)) + .case_insensitive(!case_sensitive) + .build()?; Ok(Binding { kind, pattern: pattern.to_owned(), - regex: Regex::new(&format!("^{}$", pattern))?, + regex, function: function.to_string(), cleanup: cleanup.map(String::from), }) @@ -150,7 +154,7 @@ mod test_binding { #[test] fn creates_new_without_cleanup() { - let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); + let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None, false).unwrap(); assert_eq!(b.kind(), StepKind::Given); assert!(b.regex().is_match("I am Tomjon")); assert!(!b.regex().is_match("I am Tomjon of Lancre")); @@ -166,6 +170,7 @@ mod test_binding { "I am Tomjon", "set_name", Some("unset_name"), + false, ) .unwrap(); assert_eq!(b.kind(), StepKind::Given); @@ -178,36 +183,57 @@ mod test_binding { #[test] fn equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); - let b = Binding::new(StepKind::Given, "I am Tomjon", "set_name", Some("unset")).unwrap(); + let a = Binding::new( + StepKind::Given, + "I am Tomjon", + "set_name", + Some("unset"), + false, + ) + .unwrap(); + let b = Binding::new( + StepKind::Given, + "I am Tomjon", + "set_name", + Some("unset"), + false, + ) + .unwrap(); assert_eq!(a, b); } #[test] fn not_equal() { - let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None).unwrap(); - let b = Binding::new(StepKind::Given, "I am Tomjon of Lancre", "set_name", None).unwrap(); + let a = Binding::new(StepKind::Given, "I am Tomjon", "set_name", None, false).unwrap(); + let b = Binding::new( + StepKind::Given, + "I am Tomjon of Lancre", + "set_name", + None, + false, + ) + .unwrap(); assert_ne!(a, b); } #[test] fn does_not_match_with_wrong_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "yo"); - let b = Binding::new(StepKind::When, "yo", "do_yo", None).unwrap(); + let b = Binding::new(StepKind::When, "yo", "do_yo", None, false).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn does_not_match_with_wrong_text() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); - let b = Binding::new(StepKind::Given, "bar", "yo", None).unwrap(); + let b = Binding::new(StepKind::Given, "bar", "yo", None, false).unwrap(); assert!(b.match_with_step(&step).is_none()); } #[test] fn match_with_fixed_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "foo"); - let b = Binding::new(StepKind::Given, "foo", "do_foo", None).unwrap(); + let b = Binding::new(StepKind::Given, "foo", "do_foo", None, false).unwrap(); let m = b.match_with_step(&step).unwrap(); assert_eq!(m.kind(), StepKind::Given); let mut parts = m.parts(); @@ -224,6 +250,7 @@ mod test_binding { r"I am (?P\S+), I am", "set_name", None, + false, ) .unwrap(); let m = b.match_with_step(&step).unwrap(); @@ -234,6 +261,15 @@ mod test_binding { assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured(", I am")); assert_eq!(parts.next(), None); } + + #[test] + fn case_sensitive_mismatch() { + let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); + let b = Binding::new(StepKind::Given, r"i am tomjon", "set_name", None, false).unwrap(); + assert!(b.match_with_step(&step).is_some()); + let b = Binding::new(StepKind::Given, r"i am tomjon", "set_name", None, true).unwrap(); + assert!(b.match_with_step(&step).is_none()); + } } /// Set of all known bindings. @@ -256,6 +292,8 @@ struct ParsedBinding { function: String, cleanup: Option, regex: Option, + #[serde(default)] + case_sensitive: bool, } #[derive(Debug, Deserialize)] @@ -380,6 +418,7 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result { None => None, Some(ref s) => Some(s), }, + parsed.case_sensitive, )?) } @@ -400,8 +439,14 @@ mod test_bindings { #[test] fn adds_binding() { - let binding = - Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); + let binding = Binding::new( + StepKind::Given, + r"I am (?P\S+)", + "set_name", + None, + false, + ) + .unwrap(); let mut bindings = Bindings::new(); bindings.add(binding.clone()); assert_eq!(bindings.bindings(), &[binding]); @@ -416,6 +461,9 @@ mod test_bindings { Function: declare_king - tHEn: there is applause function: check_for_applause +- given: you are alice + function: other_name + case_sensitive: true "; let mut bindings = Bindings::new(); bindings.add_from_yaml(&yaml).unwrap(); @@ -423,7 +471,9 @@ mod test_bindings { assert!(bindings.has(StepKind::Given, "I am Tomjon")); assert!(bindings.has(StepKind::When, "I declare myself king")); assert!(bindings.has(StepKind::Then, "there is applause")); - assert_eq!(bindings.len(), 3); + assert!(bindings.has(StepKind::Given, "you are alice")); + assert!(!bindings.has(StepKind::Given, "you are Alice")); + assert_eq!(bindings.len(), 4); } #[test] @@ -443,7 +493,7 @@ mod test_bindings { #[test] fn does_not_find_match_for_unmatching_kind() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::When, r"I am Tomjon", "set_foo", None).unwrap(); + let binding = Binding::new(StepKind::When, r"I am Tomjon", "set_foo", None, false).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); assert!(match bindings.find(&step) { @@ -455,8 +505,14 @@ mod test_bindings { #[test] fn does_not_find_match_for_unmatching_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = - Binding::new(StepKind::Given, r"I am Tomjon of Lancre", "set_foo", None).unwrap(); + let binding = Binding::new( + StepKind::Given, + r"I am Tomjon of Lancre", + "set_foo", + None, + false, + ) + .unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); assert!(match bindings.find(&step) { @@ -469,13 +525,15 @@ mod test_bindings { fn two_matching_bindings() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); let mut bindings = Bindings::default(); - bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", "set_foo", None).unwrap()); + bindings + .add(Binding::new(StepKind::Given, r"I am Tomjon", "set_foo", None, false).unwrap()); bindings.add( Binding::new( StepKind::Given, &super::regex_from_simple_pattern(r"I am {name}", false).unwrap(), "set_foo", None, + false, ) .unwrap(), ); @@ -488,7 +546,8 @@ mod test_bindings { #[test] fn finds_match_for_fixed_string_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = Binding::new(StepKind::Given, r"I am Tomjon", "set_name", None).unwrap(); + let binding = + Binding::new(StepKind::Given, r"I am Tomjon", "set_name", None, false).unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); let m = bindings.find(&step).unwrap(); @@ -505,8 +564,14 @@ mod test_bindings { #[test] fn finds_match_for_regexp_pattern() { let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon"); - let binding = - Binding::new(StepKind::Given, r"I am (?P\S+)", "set_name", None).unwrap(); + let binding = Binding::new( + StepKind::Given, + r"I am (?P\S+)", + "set_name", + None, + false, + ) + .unwrap(); let mut bindings = Bindings::new(); bindings.add(binding); let m = bindings.find(&step).unwrap(); -- cgit v1.2.1 From 4a745bd12a52ac12d58303f57e2660f3a051606e Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 20 Jun 2020 14:39:53 +0100 Subject: test: Add scenario to validate case sensitivity in bindings Signed-off-by: Daniel Silverstone --- subplot.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/subplot.md b/subplot.md index d975e97..b4fac99 100644 --- a/subplot.md +++ b/subplot.md @@ -1825,6 +1825,9 @@ binding. - given: a (?:broken)? binding function: a_broken_binding regex: true +- given: a capitalised Binding + function: os.getcwd + case_sensitive: true ``` ### Steps which do not match bindings do not work @@ -1850,6 +1853,28 @@ when I try to run sp-codegen --run nobinding.md -o test.py then exit code is non-zero ``` +### Steps which do not case-sensitively match sensitive bindings do not work + +~~~~{#casemismatch.md .file .markdown} +--- +title: Case sensitivity mismatch +bindings: +- badbindings.yaml +... +# Broken scenario because step has a case mismatch with sensitive binding + +```scenario +given a capitalised binding +``` +~~~~ + +```scenario +given file casemismatch.md +and file badbindings.yaml +when I try to run sp-codegen --run casemismatch.md -o test.py +then exit code is non-zero +``` + ### Steps which match more than one binding do not work ~~~~{#twobindings.md .file .markdown} -- cgit v1.2.1