FEATURE: Evaluation of an Assert Statement.

This commit is contained in:
Jeremy Wall 2018-06-04 21:45:44 -05:00
parent 9674d6006f
commit 3144adbd05
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),
// Assert statement
Assert(Expression),
Assert(Token),
}
#[cfg(test)]

View File

@ -1,23 +1,19 @@
use super::{Builder, Val};
use super::Builder;
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());
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\";",
);
}

View File

@ -285,9 +285,17 @@ impl From<String> for Val {
/// Defines a set of values in a parsed file.
type ValueMap = HashMap<Positioned<String>, Rc<Val>>;
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<Val>,
/// 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<Statement>) -> BuildResult {
for stmt in ast.iter() {
@ -505,7 +527,7 @@ impl Builder {
fn build_stmt(&mut self, stmt: &Statement) -> Result<Rc<Val>, Box<Error>> {
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<Rc<Val>, Box<Error>> {
let ok = try!(self.eval_expr(expr));
fn build_assert(&mut self, tok: &Token) -> Result<Rc<Val>, Box<Error>> {
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)
}

View File

@ -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<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());
e.cause = Some(Box::new(cause));
e.cause = Some(cause);
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>>(
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),
),
}
}

View File

@ -395,14 +395,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

View File

@ -900,13 +900,14 @@ named!(assert_statement<TokenIter, Statement, error::Error>,
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<Vec<Statement>, 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,