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),
|
Import(ImportDef),
|
||||||
|
|
||||||
// Assert statement
|
// Assert statement
|
||||||
Assert(Expression),
|
Assert(Token),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -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\";",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
16
src/error.rs
16
src/error.rs
@ -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),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
//! 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
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user