FEATURE: Evaluation of an Assert Statement.

This commit is contained in:
Jeremy Wall 2018-06-04 21:45:44 -05:00
parent 223d0cecf0
commit 2d71145813
7 changed files with 135 additions and 51 deletions

0
parse Normal file
View File

View File

@ -794,7 +794,7 @@ pub enum Statement {
Import(ImportDef), Import(ImportDef),
// Assert statement // Assert statement
Assert(Expression), Assert(Token),
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,23 +1,19 @@
use super::{Builder, Val}; use super::Builder;
use std; use std;
fn assert_build<S: Into<String>>(input: S, assert: &str) { fn assert_build(input: &str) {
let mut b = Builder::new(std::env::current_dir().unwrap()); let mut b = Builder::new(std::env::current_dir().unwrap());
b.build_file_string(input.into()).unwrap(); b.enable_validate_mode();
let result = b.eval_string(assert).unwrap(); b.eval_string(input).unwrap();
if let &Val::Boolean(ok) = result.as_ref() { if !b.assert_collector.success {
assert!(ok, format!("'{}' is not true", assert)); assert!(false, b.assert_collector.failures);
} else {
assert!(
false,
format!("'{}' does not evaluate to a boolean: {:?}", assert, result)
);
} }
} }
#[test] #[test]
fn test_comparisons() { fn test_comparisons() {
let input = " assert_build(
"
let one = 1; let one = 1;
let two = 2; let two = 2;
let foo = \"foo\"; let foo = \"foo\";
@ -34,15 +30,17 @@ fn test_comparisons() {
let list = [1, 2, 3]; let list = [1, 2, 3];
let list2 = list; let list2 = list;
let list3 = [1, 2]; let list3 = [1, 2];
"; assert \"one == one\";
assert_build(input, "one == one;"); assert \"one == one\";
assert_build(input, "one >= one;"); assert \"one >= one\";
assert_build(input, "two > one;"); assert \"two > one\";
assert_build(input, "two >= two;"); assert \"two >= two\";
assert_build(input, "tpl1 == tpl2;"); assert \"tpl1 == tpl2\";
assert_build(input, "tpl1 != tpl3;"); assert \"tpl1 != tpl3\";
assert_build(input, "list == list2;"); assert \"list == list2\";
assert_build(input, "list != list3;"); assert \"list != list3\";
",
);
} }
#[test] #[test]
@ -60,28 +58,36 @@ fn test_deep_comparison() {
let less = { let less = {
foo = \"bar\" 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);
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;");
} }
#[test] #[test]
fn test_expression_comparisons() { fn test_expression_comparisons() {
assert_build("", "2 == 1+1;"); assert_build("assert \"2 == 1+1\";");
assert_build("", "(1+1) == 2;"); assert_build("assert \"(1+1) == 2\";");
assert_build("", "(1+1) == (1+1);"); assert_build("assert \"(1+1) == (1+1)\";");
assert_build("", "(\"foo\" + \"bar\") == \"foobar\";");
} }
#[test] #[test]
fn test_binary_operator_precedence() { fn test_binary_operator_precedence() {
assert_build("let result = 2 * 2 + 1;", "result == 5;"); assert_build(
assert_build("let result = 2 + 2 * 3;", "result == 8;"); "let result = 2 * 2 + 1;
assert_build("let result = 2 * (2 + 1);", "result == 6;"); assert \"result == 5\";",
);
assert_build(
"let result = 2 + 2 * 3;
assert \"result == 8\";",
);
assert_build(
"let result = 2 * (2 + 1);
assert \"result == 6\";",
);
} }

View File

@ -285,9 +285,17 @@ impl From<String> for Val {
/// Defines a set of values in a parsed file. /// Defines a set of values in a parsed file.
type ValueMap = HashMap<Positioned<String>, Rc<Val>>; type ValueMap = HashMap<Positioned<String>, Rc<Val>>;
pub struct AssertCollector {
pub success: bool,
pub summary: String,
pub failures: String,
}
/// Handles building ucg code. /// Handles building ucg code.
pub struct Builder { pub struct Builder {
root: PathBuf, root: PathBuf,
validate_mode: bool,
assert_collector: AssertCollector,
env: Rc<Val>, env: Rc<Val>,
/// assets are other parsed files from import statements. They /// assets are other parsed files from import statements. They
/// are keyed by the normalized import path. This acts as a cache /// are keyed by the normalized import path. This acts as a cache
@ -383,6 +391,12 @@ impl Builder {
) -> Self { ) -> Self {
Builder { Builder {
root: root.into(), root: root.into(),
validate_mode: false,
assert_collector: AssertCollector {
success: true,
summary: String::new(),
failures: String::new(),
},
env: env, env: env,
assets: HashMap::new(), assets: HashMap::new(),
files: HashSet::new(), files: HashSet::new(),
@ -400,6 +414,14 @@ impl Builder {
self.lookup_sym(&key) 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. /// Builds a list of parsed UCG Statements.
pub fn build(&mut self, ast: &Vec<Statement>) -> BuildResult { pub fn build(&mut self, ast: &Vec<Statement>) -> BuildResult {
for stmt in ast.iter() { for stmt in ast.iter() {
@ -505,7 +527,7 @@ impl Builder {
fn build_stmt(&mut self, stmt: &Statement) -> Result<Rc<Val>, Box<Error>> { fn build_stmt(&mut self, stmt: &Statement) -> Result<Rc<Val>, Box<Error>> {
match stmt { 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::Let(ref def) => self.build_let(def),
&Statement::Import(ref def) => self.build_import(def), &Statement::Import(ref def) => self.build_import(def),
&Statement::Expression(ref expr) => self.eval_expr(expr), &Statement::Expression(ref expr) => self.eval_expr(expr),
@ -1111,17 +1133,53 @@ impl Builder {
))); )));
} }
fn build_assert(&self, expr: &Expression) -> Result<Rc<Val>, Box<Error>> { fn build_assert(&mut self, tok: &Token) -> Result<Rc<Val>, Box<Error>> {
let ok = try!(self.eval_expr(expr)); 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() { if let &Val::Boolean(b) = ok.as_ref() {
// record the assertion result. // record the assertion result.
if b { if b {
// success! // success!
let msg = format!(
"OK - '{}' at line: {} column: {}\n",
expr, tok.pos.line, tok.pos.column
);
self.assert_collector.summary.push_str(&msg);
} else { } else {
// failure! // 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 { } else {
// record an assertion type-failure result. // 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) Ok(ok)
} }

View File

@ -35,6 +35,7 @@ pub enum ErrorType {
UnexpectedToken, UnexpectedToken,
EmptyExpression, EmptyExpression,
ParseError, ParseError,
AssertError,
} }
impl fmt::Display for ErrorType { impl fmt::Display for ErrorType {
@ -50,6 +51,7 @@ impl fmt::Display for ErrorType {
&ErrorType::UnexpectedToken => "UnexpectedToken", &ErrorType::UnexpectedToken => "UnexpectedToken",
&ErrorType::EmptyExpression => "EmptyExpression", &ErrorType::EmptyExpression => "EmptyExpression",
&ErrorType::ParseError => "ParseError", &ErrorType::ParseError => "ParseError",
&ErrorType::AssertError => "AssertError",
}; };
w.write_str(name) w.write_str(name)
} }
@ -75,12 +77,16 @@ impl Error {
} }
} }
pub fn new_with_cause<S: Into<String>>(msg: S, t: ErrorType, cause: Error) -> Self { pub fn new_with_boxed_cause<S: Into<String>>(msg: S, t: ErrorType, cause: Box<Self>) -> Self {
let mut e = Self::new(msg, t, cause.pos.clone()); let mut e = Self::new(msg, t, cause.pos.clone());
e.cause = Some(Box::new(cause)); e.cause = Some(cause);
return e; return e;
} }
pub fn new_with_cause<S: Into<String>>(msg: S, t: ErrorType, cause: Self) -> Self {
Self::new_with_boxed_cause(msg, t, Box::new(cause))
}
pub fn new_with_errorkind<S: Into<String>>( pub fn new_with_errorkind<S: Into<String>>(
msg: S, msg: S,
t: ErrorType, t: ErrorType,
@ -89,7 +95,11 @@ impl Error {
) -> Self { ) -> Self {
match cause { match cause {
nom::ErrorKind::Custom(e) => Self::new_with_cause(msg, t, e), 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),
),
} }
} }

View File

@ -396,14 +396,23 @@
//! //!
//! The assert statement defines an expression that must evaluate to either true or false. Assert statements are noops except //! 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 //! 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 //! ```ucg
//! assert host == "www.example.com"; //! assert "host == \"www.example.com\"";
//! assert select qa, 443, { //! assert "select qa, 443, {
//! qa = 80, //! qa = 80,
//! prod = 443, //! 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 // The following is necessary to allow the macros in tokenizer and parse modules

View File

@ -900,13 +900,14 @@ named!(assert_statement<TokenIter, Statement, error::Error>,
do_parse!( do_parse!(
word!("assert") >> word!("assert") >>
pos: pos >> pos: pos >>
expr: add_return_error!( tok: add_return_error!(
nom::ErrorKind::Custom( nom::ErrorKind::Custom(
error::Error::new( error::Error::new(
"Invalid syntax for assert", "Invalid syntax for assert",
error::ErrorType::ParseError, pos)), error::ErrorType::ParseError, pos)),
expression) >> match_type!(STR)) >>
(Statement::Assert(expr)) punct!(";") >>
(Statement::Assert(tok.clone()))
) )
); );
@ -939,7 +940,7 @@ pub fn parse(input: LocatedSpan<&str>) -> Result<Vec<Statement>, error::Error> {
} }
IResult::Error(e) => { IResult::Error(e) => {
return Err(error::Error::new_with_errorkind( return Err(error::Error::new_with_errorkind(
format!("Statement Parse error: {:?} current token: {:?}", e, i_[0]), "Statement Parse error",
error::ErrorType::ParseError, error::ErrorType::ParseError,
Position { Position {
line: i_[0].pos.line, line: i_[0].pos.line,