From 2d711458139eedd4f34799e3efd623d00694f669 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 4 Jun 2018 21:45:44 -0500 Subject: [PATCH] FEATURE: Evaluation of an Assert Statement. --- parse | 0 src/ast/mod.rs | 2 +- src/build/compile_test.rs | 78 +++++++++++++++++++++------------------ src/build/mod.rs | 64 ++++++++++++++++++++++++++++++-- src/error.rs | 16 ++++++-- src/lib.rs | 17 +++++++-- src/parse/mod.rs | 9 +++-- 7 files changed, 135 insertions(+), 51 deletions(-) create mode 100644 parse diff --git a/parse b/parse new file mode 100644 index 0000000..e69de29 diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e8e5585..81d2b06 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -794,7 +794,7 @@ pub enum Statement { Import(ImportDef), // Assert statement - Assert(Expression), + Assert(Token), } #[cfg(test)] diff --git a/src/build/compile_test.rs b/src/build/compile_test.rs index 92dbaa4..0963d42 100644 --- a/src/build/compile_test.rs +++ b/src/build/compile_test.rs @@ -1,23 +1,19 @@ -use super::{Builder, Val}; +use super::Builder; use std; -fn assert_build>(input: S, assert: &str) { +fn assert_build(input: &str) { let mut b = Builder::new(std::env::current_dir().unwrap()); - b.build_file_string(input.into()).unwrap(); - let result = b.eval_string(assert).unwrap(); - if let &Val::Boolean(ok) = result.as_ref() { - assert!(ok, format!("'{}' is not true", assert)); - } else { - assert!( - false, - format!("'{}' does not evaluate to a boolean: {:?}", assert, result) - ); + b.enable_validate_mode(); + b.eval_string(input).unwrap(); + if !b.assert_collector.success { + assert!(false, b.assert_collector.failures); } } #[test] fn test_comparisons() { - let input = " + assert_build( + " let one = 1; let two = 2; let foo = \"foo\"; @@ -34,15 +30,17 @@ fn test_comparisons() { let list = [1, 2, 3]; let list2 = list; let list3 = [1, 2]; - "; - assert_build(input, "one == one;"); - assert_build(input, "one >= one;"); - assert_build(input, "two > one;"); - assert_build(input, "two >= two;"); - assert_build(input, "tpl1 == tpl2;"); - assert_build(input, "tpl1 != tpl3;"); - assert_build(input, "list == list2;"); - assert_build(input, "list != list3;"); + assert \"one == one\"; + assert \"one == one\"; + assert \"one >= one\"; + assert \"two > one\"; + assert \"two >= two\"; + assert \"tpl1 == tpl2\"; + assert \"tpl1 != tpl3\"; + assert \"list == list2\"; + assert \"list != list3\"; + ", + ); } #[test] @@ -60,28 +58,36 @@ fn test_deep_comparison() { let less = { foo = \"bar\" }; + assert \"tpl1.inner == copy.inner\"; + assert \"tpl1.inner.fld == copy.inner.fld\"; + assert \"tpl1.lst == copy.lst\"; + assert \"tpl1.foo == copy.foo\"; + assert \"tpl1 == copy\"; + assert \"tpl1 != extra\"; + assert \"tpl1 != less\"; "; - - assert_build(input, "tpl1.inner == copy.inner;"); - assert_build(input, "tpl1.inner.fld == copy.inner.fld;"); - assert_build(input, "tpl1.lst == copy.lst;"); - assert_build(input, "tpl1.foo == copy.foo;"); - assert_build(input, "tpl1 == copy;"); - assert_build(input, "tpl1 != extra;"); - assert_build(input, "tpl1 != less;"); + assert_build(input); } #[test] fn test_expression_comparisons() { - assert_build("", "2 == 1+1;"); - assert_build("", "(1+1) == 2;"); - assert_build("", "(1+1) == (1+1);"); - assert_build("", "(\"foo\" + \"bar\") == \"foobar\";"); + assert_build("assert \"2 == 1+1\";"); + assert_build("assert \"(1+1) == 2\";"); + assert_build("assert \"(1+1) == (1+1)\";"); } #[test] fn test_binary_operator_precedence() { - assert_build("let result = 2 * 2 + 1;", "result == 5;"); - assert_build("let result = 2 + 2 * 3;", "result == 8;"); - assert_build("let result = 2 * (2 + 1);", "result == 6;"); + assert_build( + "let result = 2 * 2 + 1; + assert \"result == 5\";", + ); + assert_build( + "let result = 2 + 2 * 3; + assert \"result == 8\";", + ); + assert_build( + "let result = 2 * (2 + 1); + assert \"result == 6\";", + ); } diff --git a/src/build/mod.rs b/src/build/mod.rs index c258bbc..2685de3 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -285,9 +285,17 @@ impl From for Val { /// Defines a set of values in a parsed file. type ValueMap = HashMap, Rc>; +pub struct AssertCollector { + pub success: bool, + pub summary: String, + pub failures: String, +} + /// Handles building ucg code. pub struct Builder { root: PathBuf, + validate_mode: bool, + assert_collector: AssertCollector, env: Rc, /// assets are other parsed files from import statements. They /// are keyed by the normalized import path. This acts as a cache @@ -383,6 +391,12 @@ impl Builder { ) -> Self { Builder { root: root.into(), + validate_mode: false, + assert_collector: AssertCollector { + success: true, + summary: String::new(), + failures: String::new(), + }, env: env, assets: HashMap::new(), files: HashSet::new(), @@ -400,6 +414,14 @@ impl Builder { self.lookup_sym(&key) } + /// Puts the builder in validation mode. + /// + /// Among other things this means that assertions will be evaluated and their results + /// will be saved in a report for later output. + pub fn enable_validate_mode(&mut self) { + self.validate_mode = true; + } + /// Builds a list of parsed UCG Statements. pub fn build(&mut self, ast: &Vec) -> BuildResult { for stmt in ast.iter() { @@ -505,7 +527,7 @@ impl Builder { fn build_stmt(&mut self, stmt: &Statement) -> Result, Box> { match stmt { - &Statement::Assert(ref expr) => self.build_assert(expr), + &Statement::Assert(ref expr) => self.build_assert(&expr), &Statement::Let(ref def) => self.build_let(def), &Statement::Import(ref def) => self.build_import(def), &Statement::Expression(ref expr) => self.eval_expr(expr), @@ -1111,17 +1133,53 @@ impl Builder { ))); } - fn build_assert(&self, expr: &Expression) -> Result, Box> { - let ok = try!(self.eval_expr(expr)); + fn build_assert(&mut self, tok: &Token) -> Result, Box> { + if !self.validate_mode { + // we are not in validate_mode then build_asserts are noops. + return Ok(Rc::new(Val::Empty)); + } + // FIXME(jwall): We need to append a semicolon to the expr. + let mut expr_as_stmt = String::new(); + let expr = &tok.fragment; + expr_as_stmt.push_str(expr); + expr_as_stmt.push_str(";"); + let ok = match self.eval_string(&expr_as_stmt) { + Ok(v) => v, + Err(e) => { + return Err(Box::new(error::Error::new( + format!("Assertion Evaluation of [{}] failed: {}", expr, e), + error::ErrorType::AssertError, + tok.pos.clone(), + ))); + } + }; + if let &Val::Boolean(b) = ok.as_ref() { // record the assertion result. if b { // success! + let msg = format!( + "OK - '{}' at line: {} column: {}\n", + expr, tok.pos.line, tok.pos.column + ); + self.assert_collector.summary.push_str(&msg); } else { // failure! + let msg = format!( + "NOT OK - '{}' at line: {} column: {}\n", + expr, tok.pos.line, tok.pos.column + ); + self.assert_collector.summary.push_str(&msg); + self.assert_collector.failures.push_str(&msg); + self.assert_collector.success = false; } } else { // record an assertion type-failure result. + let msg = format!( + "TYPE FAIL - '{}' at line: {} column: {}\n", + expr, tok.pos.line, tok.pos.column + ); + self.assert_collector.summary.push_str(&msg); } Ok(ok) } diff --git a/src/error.rs b/src/error.rs index be68992..77ff70a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,6 +35,7 @@ pub enum ErrorType { UnexpectedToken, EmptyExpression, ParseError, + AssertError, } impl fmt::Display for ErrorType { @@ -50,6 +51,7 @@ impl fmt::Display for ErrorType { &ErrorType::UnexpectedToken => "UnexpectedToken", &ErrorType::EmptyExpression => "EmptyExpression", &ErrorType::ParseError => "ParseError", + &ErrorType::AssertError => "AssertError", }; w.write_str(name) } @@ -75,12 +77,16 @@ impl Error { } } - pub fn new_with_cause>(msg: S, t: ErrorType, cause: Error) -> Self { + pub fn new_with_boxed_cause>(msg: S, t: ErrorType, cause: Box) -> Self { let mut e = Self::new(msg, t, cause.pos.clone()); - e.cause = Some(Box::new(cause)); + e.cause = Some(cause); return e; } + pub fn new_with_cause>(msg: S, t: ErrorType, cause: Self) -> Self { + Self::new_with_boxed_cause(msg, t, Box::new(cause)) + } + pub fn new_with_errorkind>( msg: S, t: ErrorType, @@ -89,7 +95,11 @@ impl Error { ) -> Self { match cause { nom::ErrorKind::Custom(e) => Self::new_with_cause(msg, t, e), - _ => Self::new(msg, t, pos), + e => Self::new_with_cause( + msg, + t, + Error::new(format!("ErrorKind: {}", e), ErrorType::Unsupported, pos), + ), } } diff --git a/src/lib.rs b/src/lib.rs index d6a2393..150e3db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -396,14 +396,23 @@ //! //! The assert statement defines an expression that must evaluate to either true or false. Assert statements are noops except //! during a validation compile. They give you a way to assert certains properties about your data and can be used as a form -//! of unit testting for your configurations. It starts with the assert keyword followed by a valid boolean ucg expression. +//! of unit testting for your configurations. It starts with the assert keyword followed by a quoted string that is +//! itself a valid boolean ucg expression. //! //! ```ucg -//! assert host == "www.example.com"; -//! assert select qa, 443, { +//! assert "host == \"www.example.com\""; +//! assert "select qa, 443, { //! qa = 80, //! prod = 443, -//! } == 443; +//! } == 443"; +//! ``` +//! +//! It is a little bit awkward for strings since you have to escape their quotes. But you can work around it by +//! by storing the expectations in variables first and referencing them in the assert statement. +//! +//! ```ucg +//! let expected_host = "www.example.com"; +//! assert "host == expected_host"; //! ``` // The following is necessary to allow the macros in tokenizer and parse modules diff --git a/src/parse/mod.rs b/src/parse/mod.rs index eae1385..005d6d5 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -900,13 +900,14 @@ named!(assert_statement, do_parse!( word!("assert") >> pos: pos >> - expr: add_return_error!( + tok: add_return_error!( nom::ErrorKind::Custom( error::Error::new( "Invalid syntax for assert", error::ErrorType::ParseError, pos)), - expression) >> - (Statement::Assert(expr)) + match_type!(STR)) >> + punct!(";") >> + (Statement::Assert(tok.clone())) ) ); @@ -939,7 +940,7 @@ pub fn parse(input: LocatedSpan<&str>) -> Result, error::Error> { } IResult::Error(e) => { return Err(error::Error::new_with_errorkind( - format!("Statement Parse error: {:?} current token: {:?}", e, i_[0]), + "Statement Parse error", error::ErrorType::ParseError, Position { line: i_[0].pos.line,