mirror of
https://github.com/zaphar/ucg.git
synced 2025-07-22 18:19:54 -04:00
FEATURE: Evaluation of an Assert Statement.
This commit is contained in:
parent
223d0cecf0
commit
2d71145813
@ -794,7 +794,7 @@ pub enum Statement {
|
||||
Import(ImportDef),
|
||||
|
||||
// Assert statement
|
||||
Assert(Expression),
|
||||
Assert(Token),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -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\";",
|
||||
);
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
16
src/error.rs
16
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<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),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
17
src/lib.rs
17
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
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user