From 503484a77a0bbfc5fe617c758ffa7c1218a813c6 Mon Sep 17 00:00:00 2001 From: Russell Mull Date: Mon, 9 Jul 2018 10:22:26 -0700 Subject: [PATCH 1/3] Clean up scenario parsers - Use common parser definitions from parse_utils - Factor out an eol() parser into parse utils to match either newline or eof. --- src/parse_utils.rs | 48 +++++++++++++++++++++++++++++++++++++--------- src/scenario.rs | 40 +++++++++++++------------------------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/parse_utils.rs b/src/parse_utils.rs index eafaf49..7fb41e7 100644 --- a/src/parse_utils.rs +++ b/src/parse_utils.rs @@ -5,7 +5,7 @@ use combine::Stream; use combine::ParseError; use combine::char::newline; -use combine::{Parser, many, many1, none_of, try}; +use combine::{Parser, many, many1, none_of, try, eof}; /// Match a single non-newline character /// @@ -31,9 +31,36 @@ where I: Stream, none_of("\r\n".chars()) } -/// Parse one or more characters up to the next newline character, consuming it. -/// Return the characters consumed on the way to the newline, but not the -/// newline. + +/// Parse either a newline() or an end-of-file marker, consuming any parsed +/// characters. +/// # Examples +// +/// ``` +/// # extern crate combine; +/// # extern crate rherkin; +/// # use combine::*; +/// # use rherkin::parse_utils::eol; +/// # fn main() { +/// let mut parser1 = eol(); +/// let result1 = parser1.parse("\n"); +/// assert_eq!(result1, Ok(((), ""))); +/// +/// let mut parser2 = eol(); +/// let result2 = parser2.parse(""); +/// assert_eq!(result2, Ok(((), ""))); +/// # } +/// ``` +pub fn eol() -> impl Parser +where I: Stream, + I::Error: ParseError +{ + choice! { newline().map(|_| ()), eof() } +} + +/// Parse one or more characters up to the end of line, using `eol()`. Return +/// the characters consumed on the way to the line end, but not any newline +/// character. /// /// # Examples // @@ -43,17 +70,20 @@ where I: Stream, /// # use combine::*; /// # use rherkin::parse_utils::until_eol; /// # fn main() { -/// let mut parser = until_eol(); -/// let result = parser.parse("abc\ndef"); -/// assert_eq!(result, Ok(("abc".to_string(), "def"))); +/// let mut parser1 = until_eol(); +/// let result1 = parser1.parse("abc\ndef"); +/// assert_eq!(result1, Ok(("abc".to_string(), "def"))); +/// +/// let mut parser2 = until_eol(); +/// let result2 = parser2.parse("def"); +/// assert_eq!(result2, Ok(("def".to_string(), ""))); /// # } /// ``` pub fn until_eol() -> impl Parser where I: Stream, I::Error: ParseError { - ( many1(non_newline()), newline() ) - .map(|(s, _)| s) + ( many1(non_newline()), eol()).map(|(s, _)| s) } diff --git a/src/scenario.rs b/src/scenario.rs index 3ef44b4..1fc594b 100644 --- a/src/scenario.rs +++ b/src/scenario.rs @@ -1,6 +1,7 @@ -use combine::ParseError; -use combine::Parser; -use combine::Stream; +use combine::{ParseError, Parser, Stream, many, token, optional}; +use combine::char::string; +use parse_utils::{eol, until_eol}; + use itertools; use feature::{BoxedTestCase, TestCase, TestContext}; @@ -49,11 +50,8 @@ where I: Stream, I::Error: ParseError, { - use combine::char::{newline, string}; - use combine::{many, token}; - - let first_line = (string(prefix), token(' '), inner.clone(), newline()).map(|t| t.2); - let and_line = (string("And "), inner, newline()).map(|t| t.1); + let first_line = (string(prefix), token(' '), inner.clone(), eol()).map(|t| t.2); + let and_line = (string("And "), inner, eol()).map(|t| t.1); (first_line, many(and_line)).map(|(first, mut ands): (BoxedStep, Vec>)| { ands.insert(0, first); ands @@ -80,19 +78,6 @@ where I: Stream, I::Error: ParseError, { - use combine::char::{newline, string}; - use combine::{many, none_of, optional, Parser}; - - let scenario_prefix = || string("Scenario: "); - - let non_newline = || none_of("\r\n".chars()); - - // Parse until a newline; return everything before the newline character. - let until_eol = || (many(non_newline()), newline()).map(|(s, _): (String, _)| s); - - // Parse a line with the given prefix - let prefixed_line = |prefix| (prefix, until_eol()).map(|(_, text): (_, String)| text); - let givens = scenario_block_parser("Given", given); let whens = scenario_block_parser("When", when); let thens = scenario_block_parser("Then", then); @@ -103,12 +88,13 @@ where optional(thens).map(|o| o.unwrap_or(vec![])), ).map(|(g, w, t)| itertools::concat(vec![g, w, t])); - let scenario = (prefixed_line(scenario_prefix()), steps).map( - |(name, steps): (String, Vec>)| Scenario { - name: name, - steps: steps.into_iter().map(|s| s.val).collect(), - }, - ); + let scenario = struct_parser! { + Scenario { + _: string("Scenario: "), + name: until_eol(), + steps: steps.map(|x| x.into_iter().map(|s| s.val).collect()), + } + }; scenario.map(|sc| BoxedTestCase { val: Box::new(sc) }) } From ceb0c6c07d0cca489c211be7cc85f192f3f1745d Mon Sep 17 00:00:00 2001 From: Russell Mull Date: Mon, 9 Jul 2018 14:23:27 -0700 Subject: [PATCH 2/3] Change TestCase from trait objects to enum Previously, TestCase was a trait, and features were composed of boxed TestCase trait objects. This changes it to an enum, since we expect to have only a small number of TestCase types, and users are not expected to provide new ones. This also changes the module layout; previously, the scenario and feature parsers and AST structs were each in their own modules. This makes less sense when using the enum at the TestCase level. Now all the AST structs are in the AST module, and the parsers are in the parser module. --- src/ast.rs | 77 ++++++++++++++++ src/feature.rs | 163 --------------------------------- src/lib.rs | 4 +- src/{scenario.rs => parser.rs} | 151 ++++++++++++++++++------------ tests/calculator.rs | 41 ++++++--- 5 files changed, 202 insertions(+), 234 deletions(-) create mode 100644 src/ast.rs delete mode 100644 src/feature.rs rename src/{scenario.rs => parser.rs} (62%) diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..60363c0 --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,77 @@ +/// A test context is used to pass state between different steps of a test case. +/// It may also be initialized at the feature level via a Background (TODO) +pub trait TestContext { + fn new() -> Self; +} + + +pub enum TestCase { + Background(Scenario), + Scenario(Scenario), +} + +impl TestCase { + pub fn name(&self) -> String { + match self { + TestCase::Background(s) => s.name.clone(), + TestCase::Scenario(s) => s.name.clone() + } + } + + + pub fn eval(&self, context: &mut C) -> bool { + match self { + TestCase::Background(s) => s.eval(context), + TestCase::Scenario(s) => s.eval(context) + } + } +} + +/// A feature is a collection of test cases. +pub struct Feature { + pub name: String, + pub comment: String, + pub test_cases: Vec>, +} + +impl Feature { + pub fn eval(&self) -> (bool, C) { + let mut context = C::new(); + + for tc in self.test_cases.iter() { + let pass = tc.eval(&mut context); + + if !pass { + return (false, context); + } + } + + (true, context) + } +} + +pub struct Scenario { + pub name: String, + pub steps: Vec>>, +} + +impl Scenario { + /// Execute a scenario by running each step in order, with mutable access to + /// the context. + pub fn eval(&self, context: &mut C) -> bool { + for s in self.steps.iter() { + if !s.eval(context) { + return false; + } + } + + true + } +} + +/// A specific step which makes up a scenario. Users should create their own +/// implementations of this trait, which are returned by their step parsers. +pub trait Step { + fn eval(&self, &mut C) -> bool; +} + diff --git a/src/feature.rs b/src/feature.rs deleted file mode 100644 index dbff466..0000000 --- a/src/feature.rs +++ /dev/null @@ -1,163 +0,0 @@ -use combine::ParseError; -use combine::Parser; -use combine::Stream; - -use combine::char::{newline, string}; -use combine::sep_by; -use parse_utils::{line_block, until_eol}; - -/// A test context is used to pass state between different steps of a test case. -/// It may also be initialized at the feature level via a Background (TODO) -pub trait TestContext { - fn new() -> Self; -} - -/// A test case is a generalization of a Scenario; might also be a PropTest. -pub trait TestCase { - fn name(&self) -> String; - fn eval(&self, C) -> (bool, C); -} - -/// A feature is a collection of tests. -// TODO: Implement background -pub struct Feature { - pub name: String, - pub comment: String, - pub test_cases: Vec>>, -} - -impl Feature { - pub fn eval(&self) -> (bool, C) { - let mut context = C::new(); - - for tc in self.test_cases.iter() { - let (pass, context_) = tc.eval(context); - context = context_; - - if !pass { - return (false, context); - } - } - - (true, context) - } -} - -/// The output of a test case parser. Ideally this would just be Box -/// but there's some subtle issue to do with using trait objects in an -/// associated type, perhaps in concert with the 'impl trait' feature, that -/// keeps it from working. Wrapping it in a struct is a workaround. -pub struct BoxedTestCase { - pub val: Box>, -} - -/// Construct a feature file parser, built around a test-case parser. -pub fn parser(test_case_parser: TP) -> impl Parser> -where - C: TestContext, - TP: Parser>, - I: Stream, - I::Error: ParseError, -{ - let test_cases = - sep_by(test_case_parser, newline()).map(|results: Vec>| { - results.into_iter().map(|result| result.val).collect() - }); - - struct_parser! { - Feature { - _: string("Feature: "), - name: until_eol(), - comment: line_block(), - _: newline(), - test_cases: test_cases - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use combine::stream::easy; - use combine::stream::state::State; - - /// The sample test case just records each test in this context as it runs - struct SampleTestContext { - executed_cases: Vec, - executed_contents: Vec, - } - - impl TestContext for SampleTestContext { - fn new() -> SampleTestContext { - SampleTestContext { - executed_cases: vec![], - executed_contents: vec![], - } - } - } - - struct SampleTestCase { - name: String, - content: String, - } - - impl TestCase for SampleTestCase { - fn name(&self) -> String { - self.name.clone() - } - - fn eval(&self, mut context: SampleTestContext) -> (bool, SampleTestContext) { - context.executed_cases.push(self.name.clone()); - context.executed_contents.push(self.content.clone()); - (true, context) - } - } - - fn sample_test_case_parser<'a, I>() -> impl Parser> - where - I: Stream, - I::Error: ParseError, - { - let p = struct_parser!{ - SampleTestCase { - _: string("Sample Test Case: "), - name: until_eol(), - content: line_block(), - } - }; - - p.map(|stc| BoxedTestCase { val: Box::new(stc) }) - } - - #[test] - fn feature() { - let s = "Feature: my feature\n\ - comment line one\n\ - comment line two\n\ - \n\ - Sample Test Case: first\n\ - Content one\n\ - \n\ - Sample Test Case: second\n\ - Content two\n"; - - let (feat, remaining) = parser(sample_test_case_parser()) - .easy_parse(State::new(s)) - .unwrap(); - println!("End state: {:#?}", remaining); - - assert_eq!(feat.name, "my feature".to_string()); - assert_eq!(feat.comment, "comment line one\ncomment line two".to_string()); - assert_eq!(feat.test_cases.len(), 2); - assert_eq!(feat.test_cases[0].name(), "first".clone()); - assert_eq!(feat.test_cases[1].name(), "second".clone()); - - let (pass, ctx) = feat.eval(); - assert!(pass); - assert_eq!(ctx.executed_cases, - vec!("first".to_string(), "second".to_string())); - assert_eq!(ctx.executed_contents, - vec!("Content one".to_string(), "Content two".to_string())); - } -} diff --git a/src/lib.rs b/src/lib.rs index 8712d46..a149372 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,8 @@ extern crate combine; extern crate itertools; -pub mod feature; -pub mod scenario; +pub mod parser; +pub mod ast; pub mod parse_utils; diff --git a/src/scenario.rs b/src/parser.rs similarity index 62% rename from src/scenario.rs rename to src/parser.rs index 1fc594b..0a667c1 100644 --- a/src/scenario.rs +++ b/src/parser.rs @@ -1,46 +1,19 @@ -use combine::{ParseError, Parser, Stream, many, token, optional}; -use combine::char::string; -use parse_utils::{eol, until_eol}; - +use ast::{Step, TestContext, TestCase, Feature, Scenario}; use itertools; -use feature::{BoxedTestCase, TestCase, TestContext}; - -struct Scenario { - name: String, - steps: Vec>>, -} +use combine::ParseError; +use combine::Parser; +use combine::Stream; -/// A specific step which makes up a test context. Users should create there own -/// implementations of this trait, which are returned by their step parsers. -pub trait Step { - fn eval(&self, &mut C) -> bool; -} - -impl TestCase for Scenario { - fn name(&self) -> String { - self.name.clone() - } - - /// Execute a scenario by creating a new test context, then running each - /// step in order with mutable access to the context. - fn eval(&self, mut context: C) -> (bool, C) { - // let mut ctx = TC::new(); - for s in self.steps.iter() { - if !s.eval(&mut context) { - return (false, context); - } - } - - (true, context) - } -} +use combine::char::{newline, string}; +use combine::{many, many1, sep_by, optional, token}; +use parse_utils::{line_block, until_eol, eol}; pub struct BoxedStep { pub val: Box>, } -fn scenario_block_parser( +fn scenario_block( prefix: &'static str, inner: P, ) -> impl Parser>> @@ -58,6 +31,7 @@ where }) } + /// A `TestCase` parser for classic Cucumber-style Scenarios; this parser (or a /// composition thereof) should be passed to feature::parser. /// @@ -65,11 +39,11 @@ where /// /// * `given`, `when`, `then` : User-defined parsers to parse and produce /// `Step`s out of the text after `Given`, `When`, and `Then`, respectively. -pub fn parser( +pub fn scenario( given: GP, when: WP, then: TP, -) -> impl Parser> +) -> impl Parser> where TC: TestContext + 'static, GP: Parser> + Clone, @@ -78,9 +52,9 @@ where I: Stream, I::Error: ParseError, { - let givens = scenario_block_parser("Given", given); - let whens = scenario_block_parser("When", when); - let thens = scenario_block_parser("Then", then); + let givens = scenario_block("Given", given); + let whens = scenario_block("When", when); + let thens = scenario_block("Then", then); let steps = ( optional(givens).map(|o| o.unwrap_or(vec![])), @@ -96,14 +70,39 @@ where } }; - scenario.map(|sc| BoxedTestCase { val: Box::new(sc) }) + scenario.map(|s| TestCase::Scenario(s)) +} + + +/// Construct a feature file parser, built around a test-case parser. +pub fn feature(test_case_parser: TP) -> impl Parser> +where + C: TestContext, + TP: Parser>, + I: Stream, + I::Error: ParseError, +{ + let blank_lines = || many1::, _>(newline()); + + let test_cases = sep_by(test_case_parser, blank_lines()); + + struct_parser! { + Feature { + _: optional(blank_lines()), + _: string("Feature: "), + name: until_eol(), + comment: line_block(), + _: blank_lines(), + test_cases: test_cases + } + } } + + #[cfg(test)] mod tests { use super::*; - - use feature; use combine::stream::state::State; /// The sample test case just records each step as it runs @@ -130,20 +129,7 @@ mod tests { } } - #[test] - fn scenario() { - let s = "Feature: my feature\n\ - \n\ - Scenario: One\n\ - Given G1\n\ - When W1\n\ - Then T1\n\ - \n\ - Scenario: Two\n\ - Given G2\n\ - When W2\n\ - Then T2\n"; - + fn do_parse(s: &str) -> Feature { use combine::char::digit; use combine::token; @@ -152,14 +138,62 @@ mod tests { let when = struct_parser! { SampleStep { _: token('W'), num: num_digit() } }; let then = struct_parser! { SampleStep { _: token('T'), num: num_digit() } }; - let (feat, remaining) = feature::parser(parser( + let (feat, remaining) = feature(scenario( given.map(|x| BoxedStep { val: Box::new(x) }), when.map(|x| BoxedStep { val: Box::new(x) }), then.map(|x| BoxedStep { val: Box::new(x) }), )).easy_parse(State::new(s)) .unwrap(); + println!("End state: {:#?}", remaining); + feat + } + + #[test] + fn test_parse() { + let feat = do_parse(r" +Feature: my feature +one +two + +Scenario: One +Given G1 +When W1 +Then T1 + +Scenario: Two +Given G2 +When W2 +Then T2"); + + assert_eq!(feat.name, "my feature".to_string()); + assert_eq!(feat.comment, "one\ntwo".to_string()); + assert_eq!(feat.test_cases.len(), 2); + assert_eq!(feat.test_cases[0].name(), "One".clone()); + assert_eq!(feat.test_cases[1].name(), "Two".clone()); + + let (pass, ctx) = feat.eval(); + assert!(pass); + assert_eq!(ctx.executed_steps, vec![1, 1, 1, 2, 2, 2]); + } + + #[test] + fn test_parse_extra_whitespace() { + let feat = do_parse(r" +Feature: my feature + +Scenario: One +Given G1 +When W1 +Then T1 + +Scenario: Two +Given G2 +When W2 +Then T2 +"); + assert_eq!(feat.name, "my feature".to_string()); assert_eq!(feat.comment, "".to_string()); assert_eq!(feat.test_cases.len(), 2); @@ -170,4 +204,5 @@ mod tests { assert!(pass); assert_eq!(ctx.executed_steps, vec![1, 1, 1, 2, 2, 2]); } + } diff --git a/tests/calculator.rs b/tests/calculator.rs index 76ff22c..46a193e 100644 --- a/tests/calculator.rs +++ b/tests/calculator.rs @@ -6,8 +6,9 @@ use combine::stream::state::State; use combine::char::{string, digit}; extern crate rherkin; -use rherkin::feature::{self, TestContext}; -use rherkin::scenario::{self, Step, BoxedStep}; +//use rherkin::feature; +//use rherkin::scenario::{self, Step, BoxedStep, TestContext}; +use rherkin::{ast, parser}; // An rpn calculator, something we can write tests for. #[derive(Debug)] @@ -80,7 +81,7 @@ impl Calculator { } } -impl TestContext for Calculator { +impl ast::TestContext for Calculator { fn new() -> Calculator { Calculator { current: vec!(), @@ -93,7 +94,7 @@ mod steps { use super::*; pub struct Clear { } - impl Step for Clear { + impl ast::Step for Clear { fn eval(&self, calc: &mut Calculator) -> bool { println!("Clear"); calc.current = vec!(); @@ -103,7 +104,7 @@ mod steps { } pub struct Press { pub button: Button } - impl Step for Press { + impl ast::Step for Press { fn eval(&self, calc: &mut Calculator) -> bool { println!("Press {:?}", self.button); calc.press(&self.button) @@ -111,7 +112,7 @@ mod steps { } pub struct CheckDisplay { pub expected: String } - impl Step for CheckDisplay { + impl ast::Step for CheckDisplay { fn eval(&self, calc: &mut Calculator) -> bool { let actual = calc.stack.last(); println!("Check display: expected {:?}, actual {:#?}", self.expected, actual); @@ -177,11 +178,11 @@ Then the display should read 2 let then = choice! { check_display }; let mut p = - feature::parser( - scenario::parser( - given.map(|x| BoxedStep { val: Box::new(x) }), - when.map(|x| BoxedStep { val: Box::new(x) }), - then.map(|x| BoxedStep { val: Box::new(x) }))); + parser::feature( + parser::scenario( + given.map(|x| parser::BoxedStep { val: Box::new(x) }), + when.map (|x| parser::BoxedStep { val: Box::new(x) }), + then.map (|x| parser::BoxedStep { val: Box::new(x) }))); let (f, remaining) = p.easy_parse(State::new(spec)).unwrap(); @@ -189,3 +190,21 @@ Then the display should read 2 assert!(success); } + +// fn proptests() { +// let spec = r#" +// Feature: RPN Calculator Property Specs + +// PropSpec: arbitrary addition +// Given a fresh calculator +// And a number A less than 10000 +// And a number B less than 10000 +// When I enter the number A +// And I press enter +// And I enter the number B +// And I press plus +// Then the displayed value should be less than 20000 +// "#; + +// assert!(true) +// } From 3e951fd987d916882e523a0e555d446ac79e42d3 Mon Sep 17 00:00:00 2001 From: Russell Mull Date: Tue, 10 Jul 2018 10:46:07 -0700 Subject: [PATCH 3/3] Add support for Backgrounds This adds support for a Background section in the spec, which is executed before each test case to initialize its context. - Add a TestResult struct, used at the TestCase level to bundle together success, the test case name, and the test context. --- src/ast.rs | 59 ++++++++++++++----- src/parser.rs | 140 ++++++++++++++++++++++++++++---------------- tests/calculator.rs | 15 ++--- 3 files changed, 141 insertions(+), 73 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 60363c0..0d54363 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,5 +1,4 @@ /// A test context is used to pass state between different steps of a test case. -/// It may also be initialized at the feature level via a Background (TODO) pub trait TestContext { fn new() -> Self; } @@ -11,7 +10,7 @@ pub enum TestCase { } impl TestCase { - pub fn name(&self) -> String { + pub fn name(&self) -> Option { match self { TestCase::Background(s) => s.name.clone(), TestCase::Scenario(s) => s.name.clone() @@ -19,7 +18,7 @@ impl TestCase { } - pub fn eval(&self, context: &mut C) -> bool { + pub fn eval(&self, context: C) -> TestResult { match self { TestCase::Background(s) => s.eval(context), TestCase::Scenario(s) => s.eval(context) @@ -27,45 +26,77 @@ impl TestCase { } } +pub struct TestResult { + pub test_case_name: String, + pub pass: bool, + pub context: C +} + /// A feature is a collection of test cases. pub struct Feature { pub name: String, pub comment: String, + pub background: Option>, pub test_cases: Vec>, } impl Feature { - pub fn eval(&self) -> (bool, C) { - let mut context = C::new(); + pub fn eval(&self) -> Vec> { + let mut results = vec![]; for tc in self.test_cases.iter() { - let pass = tc.eval(&mut context); + let mut context = C::new(); - if !pass { - return (false, context); + if let Some(TestCase::Background(ref bg)) = self.background { + match bg.eval(context) { + mut r @ TestResult { pass: false, ..} => { + r.test_case_name = "".to_string(); + results.push(r); + continue; + }, + TestResult { pass: true, context: c, ..} => { + context = c; + } + } } + + results.push(tc.eval(context)); } - (true, context) + results } } pub struct Scenario { - pub name: String, + pub name: Option, pub steps: Vec>>, } impl Scenario { /// Execute a scenario by running each step in order, with mutable access to /// the context. - pub fn eval(&self, context: &mut C) -> bool { + pub fn eval(&self, mut context: C) -> TestResult { for s in self.steps.iter() { - if !s.eval(context) { - return false; + if !s.eval(&mut context) { + return TestResult { + test_case_name: match self.name.as_ref() { + Some(s) => s.clone(), + None => "".to_string() + }, + pass: false, + context: context + }; } } - true + TestResult { + test_case_name: match self.name.as_ref() { + Some(s) => s.clone(), + None => "".to_string() + }, + pass: true, + context: context + } } } diff --git a/src/parser.rs b/src/parser.rs index 0a667c1..295cfc1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,4 @@ -use ast::{Step, TestContext, TestCase, Feature, Scenario}; +use ast::{Feature, Scenario, Step, TestCase, TestContext}; use itertools; use combine::ParseError; @@ -6,8 +6,8 @@ use combine::Parser; use combine::Stream; use combine::char::{newline, string}; -use combine::{many, many1, sep_by, optional, token}; -use parse_utils::{line_block, until_eol, eol}; +use combine::{many, many1, optional, sep_by, token}; +use parse_utils::{eol, line_block, until_eol}; pub struct BoxedStep { pub val: Box>, @@ -31,24 +31,17 @@ where }) } - -/// A `TestCase` parser for classic Cucumber-style Scenarios; this parser (or a -/// composition thereof) should be passed to feature::parser. -/// -/// # Arguments -/// -/// * `given`, `when`, `then` : User-defined parsers to parse and produce -/// `Step`s out of the text after `Given`, `When`, and `Then`, respectively. -pub fn scenario( +fn scenario( + prefix: &'static str, given: GP, when: WP, then: TP, -) -> impl Parser> +) -> impl Parser> where - TC: TestContext + 'static, - GP: Parser> + Clone, - WP: Parser> + Clone, - TP: Parser> + Clone, + C: TestContext + 'static, + GP: Parser> + Clone, + WP: Parser> + Clone, + TP: Parser> + Clone, I: Stream, I::Error: ParseError, { @@ -62,29 +55,50 @@ where optional(thens).map(|o| o.unwrap_or(vec![])), ).map(|(g, w, t)| itertools::concat(vec![g, w, t])); - let scenario = struct_parser! { + struct_parser! { Scenario { - _: string("Scenario: "), - name: until_eol(), + _: string(prefix), + _: string(":"), + name: choice!( + until_eol().map(|s| Some(s.trim().to_string())), + newline().map(|_| None) + ), steps: steps.map(|x| x.into_iter().map(|s| s.val).collect()), } - }; - - scenario.map(|s| TestCase::Scenario(s)) + } } - -/// Construct a feature file parser, built around a test-case parser. -pub fn feature(test_case_parser: TP) -> impl Parser> +/// Construct a feature file parser, built around step parsers +/// +/// # Arguments +/// +/// * `given`, `when`, `then` : User-defined parsers to parse and produce +/// `Step`s out of the text after `Given`, `When`, and `Then`, respectively. +pub fn feature( + given: GP, + when: WP, + then: TP, +) -> impl Parser> where - C: TestContext, - TP: Parser>, + C: TestContext + 'static, + GP: Parser> + Clone, + WP: Parser> + Clone, + TP: Parser> + Clone, I: Stream, I::Error: ParseError, { let blank_lines = || many1::, _>(newline()); - let test_cases = sep_by(test_case_parser, blank_lines()); + let background = optional( + ( + scenario("Background", given.clone(), when.clone(), then.clone()), + blank_lines(), + ).map(|t| TestCase::Background(t.0)) + ); + + let test_cases = sep_by( + scenario("Scenario", given, when, then).map(|s| TestCase::Scenario(s)), + blank_lines()); struct_parser! { Feature { @@ -93,13 +107,12 @@ where name: until_eol(), comment: line_block(), _: blank_lines(), + background: background, test_cases: test_cases } } } - - #[cfg(test)] mod tests { use super::*; @@ -138,11 +151,11 @@ mod tests { let when = struct_parser! { SampleStep { _: token('W'), num: num_digit() } }; let then = struct_parser! { SampleStep { _: token('T'), num: num_digit() } }; - let (feat, remaining) = feature(scenario( + let (feat, remaining) = feature( given.map(|x| BoxedStep { val: Box::new(x) }), when.map(|x| BoxedStep { val: Box::new(x) }), then.map(|x| BoxedStep { val: Box::new(x) }), - )).easy_parse(State::new(s)) + ).easy_parse(State::new(s)) .unwrap(); println!("End state: {:#?}", remaining); @@ -152,57 +165,80 @@ mod tests { #[test] fn test_parse() { - let feat = do_parse(r" + let feat = do_parse( + r" Feature: my feature one two -Scenario: One +Background: Given G1 -When W1 -Then T1 + +Scenario: One +Given G2 +When W3 +Then T4 Scenario: Two Given G2 -When W2 -Then T2"); +And G3 +When W4 +And W5 +Then T6 +And T7"); assert_eq!(feat.name, "my feature".to_string()); assert_eq!(feat.comment, "one\ntwo".to_string()); + assert!(feat.background.is_some()); assert_eq!(feat.test_cases.len(), 2); - assert_eq!(feat.test_cases[0].name(), "One".clone()); - assert_eq!(feat.test_cases[1].name(), "Two".clone()); + assert_eq!(feat.test_cases[0].name(), Some("One".to_string())); + assert_eq!(feat.test_cases[1].name(), Some("Two".to_string())); + + let results = feat.eval(); - let (pass, ctx) = feat.eval(); - assert!(pass); - assert_eq!(ctx.executed_steps, vec![1, 1, 1, 2, 2, 2]); + assert_eq!(results[0].pass, true); + assert_eq!(results[0].test_case_name, "One".to_string()); + assert_eq!(results[0].context.executed_steps, [1, 2, 3, 4]); + + assert_eq!(results[1].pass, true); + assert_eq!(results[1].test_case_name, "Two".to_string()); + assert_eq!(results[1].context.executed_steps, [1, 2, 3, 4, 5, 6, 7]); } #[test] fn test_parse_extra_whitespace() { - let feat = do_parse(r" + let feat = do_parse( + r" Feature: my feature + Scenario: One Given G1 When W1 Then T1 + Scenario: Two Given G2 When W2 Then T2 -"); +", + ); assert_eq!(feat.name, "my feature".to_string()); assert_eq!(feat.comment, "".to_string()); assert_eq!(feat.test_cases.len(), 2); - assert_eq!(feat.test_cases[0].name(), "One".clone()); - assert_eq!(feat.test_cases[1].name(), "Two".clone()); + assert_eq!(feat.test_cases[0].name(), Some("One".to_string())); + assert_eq!(feat.test_cases[1].name(), Some("Two".to_string())); - let (pass, ctx) = feat.eval(); - assert!(pass); - assert_eq!(ctx.executed_steps, vec![1, 1, 1, 2, 2, 2]); - } + let results = feat.eval(); + assert_eq!(results[0].pass, true); + assert_eq!(results[0].test_case_name, "One".to_string()); + assert_eq!(results[0].context.executed_steps, [1, 1, 1]); + + assert_eq!(results[1].pass, true); + assert_eq!(results[1].test_case_name, "Two".to_string()); + assert_eq!(results[1].context.executed_steps, [2, 2, 2]); + } } diff --git a/tests/calculator.rs b/tests/calculator.rs index 46a193e..30581d0 100644 --- a/tests/calculator.rs +++ b/tests/calculator.rs @@ -179,15 +179,16 @@ Then the display should read 2 let mut p = parser::feature( - parser::scenario( - given.map(|x| parser::BoxedStep { val: Box::new(x) }), - when.map (|x| parser::BoxedStep { val: Box::new(x) }), - then.map (|x| parser::BoxedStep { val: Box::new(x) }))); + given.map(|x| parser::BoxedStep { val: Box::new(x) }), + when.map (|x| parser::BoxedStep { val: Box::new(x) }), + then.map (|x| parser::BoxedStep { val: Box::new(x) })); - let (f, remaining) = p.easy_parse(State::new(spec)).unwrap(); + let (f, _remaining) = p.easy_parse(State::new(spec)).unwrap(); - let (success, _) = f.eval(); - assert!(success); + let results = f.eval(); + for r in results { + assert!(r.pass); + } }