FEATURE: Better error reporting.

Slight change to how assert works to support this. We no longer automatically add
a semicolon to the expressions we require the user to right them. This updates the
docs to illustrate that and reformats our integration test suite for this and
readability.
This commit is contained in:
Jeremy Wall 2018-11-06 19:40:56 -06:00
parent d254ff3f94
commit d2f0ea9f24
17 changed files with 252 additions and 100 deletions

View File

View File

@ -0,0 +1 @@
import

View File

@ -2,12 +2,14 @@ assert |macro |;
assert |{ foo = hello world}|; assert |{ foo = hello world}|;
assert |{ foo = "hello world"|; assert |{ foo = "hello world"|;
assert |{ = }|; assert |{ = }|;
assert |let foo |;
assert |let |;
assert |import |;
assert |import foo |;
assert |out |;
assert |out "|;
assert |out json|;
assert |"|; assert |"|;
assert |=|; assert |=|;
assert |let |;
assert |let foo |;
assert |let foo =|;
//assert |import |;
//assert |import foo |;
//assert |out |;
//assert |out "|;
//assert |out json|;

View File

@ -16,15 +16,33 @@ let list = [1, 2, 3];
let list2 = list; let list2 = list;
let list3 = [1, 2]; let list3 = [1, 2];
assert |one == one|; assert |
assert |one == one|; one == one;
assert |one >= one|; |;
assert |two > one|; assert |
assert |two >= two|; one == one;
assert |tpl1 == tpl2|; |;
assert |tpl1 != tpl3|; assert |
assert |list == list2|; one >= one;
assert |list != list3|; |;
assert |
two > one;
|;
assert |
two >= two;
|;
assert |
tpl1 == tpl2;
|;
assert |
tpl1 != tpl3;
|;
assert |
list == list2;
|;
assert |
list != list3;
|;
// Deep Comparisons // Deep Comparisons
let tpl4 = { let tpl4 = {
@ -40,17 +58,33 @@ let less = {
foo = "bar" foo = "bar"
}; };
assert |tpl4.inner == copy.inner|; assert |
assert |tpl4.inner.fld == copy.inner.fld|; tpl4.inner == copy.inner;
assert |tpl4.lst == copy.lst|; |;
assert |tpl4.foo == copy.foo|; assert |
assert |tpl4 == copy|; tpl4.inner.fld == copy.inner.fld;
assert |tpl4 != extra|; |;
assert |tpl4 != less|; assert |
tpl4.lst == copy.lst;
|;
assert |
tpl4.foo == copy.foo;
|;
assert |
tpl4 == copy;
|;
assert |
tpl4 != extra;
|;
assert |
tpl4 != less;
|;
// Expression comparisons // Expression comparisons
assert |2 == 1+1|; assert |2 == 1+1;|;
assert |(1+1) == 2|; assert |(1+1) == 2;|;
assert |(1+1) == (1+1)|; assert |(1+1) == (1+1);|;
let want = "foo"; let want = "foo";
assert |select want, 1, { foo=2, } == 2|; assert |
select want, 1, { foo=2, } == 2;
|;

View File

@ -1,2 +1,6 @@
assert |"hello " + "world" == "hello world"|; assert |
assert |[1, 2, 3] + [4, 5, 6] == [1, 2, 3, 4, 5, 6]|; "hello " + "world" == "hello world";
|;
assert |
[1, 2, 3] + [4, 5, 6] == [1, 2, 3, 4, 5, 6];
|;

View File

@ -2,4 +2,6 @@ let empty = NULL;
let tpl = { let tpl = {
foo = NULL, foo = NULL,
}; };
assert |tpl.foo == empty|; assert |
tpl.foo == empty;
|;

View File

@ -1,4 +1,12 @@
assert |"hello @" % ("world") == "hello world"|; assert |
assert |"1 @ @" % (2, 3) == "1 2 3"|; "hello @" % ("world") == "hello world";
assert |"@ or @" % (true, false) == "true or false"|; |;
assert |"@" % (NULL) == "NULL"|; assert |
"1 @ @" % (2, 3) == "1 2 3";
|;
assert |
"@ or @" % (true, false) == "true or false";
|;
assert |
"@" % (NULL) == "NULL";
|;

View File

@ -12,11 +12,25 @@ let boolfiltrator = macro(item) => {
result = item < 5, result = item < 5,
}; };
assert |map mapper.result list1 == [2, 3, 4, 5]|; assert |
assert |(map mapper.result [1, 2, 3, 4]) == [2, 3, 4, 5]|; map mapper.result list1 == [2, 3, 4, 5];
assert |map mapper.result [1, 2, 3, 4] == [2, 3, 4, 5]|; |;
assert |
(map mapper.result [1, 2, 3, 4]) == [2, 3, 4, 5];
|;
assert |
map mapper.result [1, 2, 3, 4] == [2, 3, 4, 5];
|;
assert |filter filtrator.result list2 == ["foo", "foo"]|; assert |
assert |(filter filtrator.result ["foo", "bar", "foo", "bar"]) == ["foo", "foo"]|; filter filtrator.result list2 == ["foo", "foo"];
assert |filter filtrator.result ["foo", "bar", "foo", "bar"] == ["foo", "foo"]|; |;
assert |filter boolfiltrator.result [1, 2, 3, 4, 5, 6, 7] == [1, 2, 3, 4]|; assert |
(filter filtrator.result ["foo", "bar", "foo", "bar"]) == ["foo", "foo"];
|;
assert |
filter filtrator.result ["foo", "bar", "foo", "bar"] == ["foo", "foo"];
|;
assert |
filter boolfiltrator.result [1, 2, 3, 4, 5, 6, 7] == [1, 2, 3, 4];
|;

View File

@ -14,11 +14,25 @@ let cplxmacro = macro(argint, argstr, argfloat) => {
let simpleresult = simplemacro(1, 2, 3); let simpleresult = simplemacro(1, 2, 3);
let cplxresult = cplxmacro(1, "We", 3.0); let cplxresult = cplxmacro(1, "We", 3.0);
assert |simpleresult.field1 == 1|; assert |
assert |simpleresult.field2 == 2|; simpleresult.field1 == 1;
assert |simpleresult.field3 == 3|; |;
assert |
simpleresult.field2 == 2;
|;
assert |
simpleresult.field3 == 3;
|;
assert |cplxresult.field1 == 2|; assert |
assert |cplxresult.field2 == "We are here"|; cplxresult.field1 == 2;
assert |cplxresult.field3 == 2.0|; |;
assert |cplxresult.boolfield == true|; assert |
cplxresult.field2 == "We are here";
|;
assert |
cplxresult.field3 == 2.0;
|;
assert |
cplxresult.boolfield == true;
|;

View File

@ -1,9 +1,27 @@
assert |2 * 2 + 1 == 5|; assert |
assert |2 + 2 * 3 == 8|; 2 * 2 + 1 == 5;
assert |2 * (2 + 1) == 6|; |;
assert |2 * 2 + 1 > 4|; assert |
assert |2 * 2 + 1 < 6|; 2 + 2 * 3 == 8;
assert |2 * 2 + 1 >= 5|; |;
assert |2 * 2 + 1 <= 5|; assert |
assert |2 / 2 == 1|; 2 * (2 + 1) == 6;
assert |2 - 1 == 1|; |;
assert |
2 * 2 + 1 > 4;
|;
assert |
2 * 2 + 1 < 6;
|;
assert |
2 * 2 + 1 >= 5;
|;
assert |
2 * 2 + 1 <= 5;
|;
assert |
2 / 2 == 1;
|;
assert |
2 - 1 == 1;
|;

View File

@ -11,8 +11,12 @@ let defaultgot = select badwant, "OOPS", {
door2 = "you lose", door2 = "you lose",
}; };
assert |got == "grand prize"|; assert |
assert |defaultgot == "OOPS"|; got == "grand prize";
|;
assert |
defaultgot == "OOPS";
|;
// select inside a macro // select inside a macro
@ -25,6 +29,12 @@ let condmacro = macro(arg) => {
let result = condmacro("opt1"); let result = condmacro("opt1");
assert |condmacro("opt1") == {output = "yay"}|; assert |
assert |condmacro("opt2") == {output = "boo"}|; condmacro("opt1") == {output = "yay"};
assert |condmacro("invalid") == {output = NULL}|; |;
assert |
condmacro("opt2") == {output = "boo"};
|;
assert |
condmacro("invalid") == {output = NULL};
|;

View File

@ -10,10 +10,24 @@ let testmacro = macro(arg) => {
output = arg, output = arg,
}; };
assert |list.0 == 1|; assert |
assert |list.1 == 2|; list.0 == 1;
assert |list.3 == 4|; |;
assert |tuple.field1 == 1|; assert |
assert |tuple.field2 == 3|; list.1 == 2;
assert |tuple.deeplist.0 == "foo"|; |;
assert |tuple.deeplist.1 == "bar"|; assert |
list.3 == 4;
|;
assert |
tuple.field1 == 1;
|;
assert |
tuple.field2 == 3;
|;
assert |
tuple.deeplist.0 == "foo";
|;
assert |
tuple.deeplist.1 == "bar";
|;

View File

@ -14,11 +14,27 @@ let nestedtpl = {
list = [1, 2, 3, 4], list = [1, 2, 3, 4],
}; };
assert |simpletpl.foo == "bar"|; assert |
assert |stringfieldtpl."field 1" == 1|; simpletpl.foo == "bar";
assert |nestedtpl.scalar == 1|; |;
assert |nestedtpl.inner.field == "value"|; assert |
assert |nestedtpl.list.0 == 1|; stringfieldtpl."field 1" == 1;
assert |nestedtpl.list.1 == 2|; |;
assert |nestedtpl.list.2 == 3|; assert |
assert |nestedtpl.list.3 == 4|; nestedtpl.scalar == 1;
|;
assert |
nestedtpl.inner.field == "value";
|;
assert |
nestedtpl.list.0 == 1;
|;
assert |
nestedtpl.list.1 == 2;
|;
assert |
nestedtpl.list.2 == 3;
|;
assert |
nestedtpl.list.3 == 4;
|;

View File

@ -25,6 +25,8 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::string::ToString; use std::string::ToString;
use simple_error;
use ast::*; use ast::*;
use error; use error;
use format; use format;
@ -248,7 +250,7 @@ impl<'a> Builder<'a> {
Ok(()) Ok(())
} }
fn eval_span(&mut self, input: OffsetStrIter) -> Result<Rc<Val>, Box<Error>> { fn eval_input(&mut self, input: OffsetStrIter) -> Result<Rc<Val>, Box<Error>> {
match parse(input.clone()) { match parse(input.clone()) {
Ok(stmts) => { Ok(stmts) => {
//panic!("Successfully parsed {}", input); //panic!("Successfully parsed {}", input);
@ -271,7 +273,7 @@ impl<'a> Builder<'a> {
/// Evaluate an input string as UCG. /// Evaluate an input string as UCG.
pub fn eval_string(&mut self, input: &str) -> Result<Rc<Val>, Box<Error>> { pub fn eval_string(&mut self, input: &str) -> Result<Rc<Val>, Box<Error>> {
self.eval_span(OffsetStrIter::new(input)) self.eval_input(OffsetStrIter::new(input))
} }
/// Builds a ucg file at the named path. /// Builds a ucg file at the named path.
@ -280,8 +282,19 @@ impl<'a> Builder<'a> {
let mut f = try!(File::open(name)); let mut f = try!(File::open(name));
let mut s = String::new(); let mut s = String::new();
try!(f.read_to_string(&mut s)); try!(f.read_to_string(&mut s));
self.last = Some(try!(self.eval_string(&s))); let eval_result = self.eval_string(&s);
Ok(()) match eval_result {
Ok(v) => {
self.last = Some(v);
Ok(())
}
Err(e) => {
let err = simple_error::SimpleError::new(
format!("Error building file: {}\n{}", name, e.as_ref()).as_ref(),
);
Err(Box::new(err))
}
}
} }
fn build_import(&mut self, def: &ImportDef) -> Result<Rc<Val>, Box<Error>> { fn build_import(&mut self, def: &ImportDef) -> Result<Rc<Val>, Box<Error>> {
@ -993,13 +1006,10 @@ impl<'a> Builder<'a> {
// we are not in validate_mode then build_asserts are noops. // we are not in validate_mode then build_asserts are noops.
return Ok(Rc::new(Val::Empty)); return Ok(Rc::new(Val::Empty));
} }
let mut expr_as_stmt = String::new();
let expr = &tok.fragment; let expr = &tok.fragment;
expr_as_stmt.push_str(expr);
expr_as_stmt.push_str(";");
let assert_input = let assert_input =
OffsetStrIter::new_with_offsets(&expr_as_stmt, tok.pos.line - 1, tok.pos.column - 1); OffsetStrIter::new_with_offsets(expr, tok.pos.line - 1, tok.pos.column - 1);
let ok = match self.eval_span(assert_input) { let ok = match self.eval_input(assert_input) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
// failure! // failure!

View File

@ -409,15 +409,19 @@
//! //!
//! 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 testing for your configurations. It starts with the assert keyword followed by a valid boolean ucg expression //! of unit testing for your configurations. It starts with the assert keyword followed by a valid block of ucg statements
//! delimited by `|` characters. //! delimited by `|` characters. The final statement in the in the block must evaluate to a boolean expression.
//! //!
//! ```ucg //! ```ucg
//! assert host == "www.example.com"; //! assert |
//! assert |select qa, 443, { //! host == "www.example.com";
//! qa = 80, //! |;
//! prod = 443, //! assert |
//! } == 443|; //! select qa, 443, {
//! qa = 80,
//! prod = 443,
//! } == 443;
//! |;
//! ``` //! ```
//! //!
//! When _test.ucg files are run in a validation run then ucg will output a log of all the assertions //! When _test.ucg files are run in a validation run then ucg will output a log of all the assertions

View File

@ -843,9 +843,10 @@ fn tuple_to_let(tok: Token, expr: Expression) -> Statement {
make_fn!( make_fn!(
let_stmt_body<SliceIter<Token>, Statement>, let_stmt_body<SliceIter<Token>, Statement>,
do_each!( do_each!(
name => match_type!(BAREWORD), name => wrap_err!(match_type!(BAREWORD), "Expected name for binding"),
_ => punct!("="), _ => punct!("="),
val => trace_nom!(expression), // TODO(jwall): Wrap this error with an appropriate abortable_parser::Error
val => wrap_err!(trace_nom!(expression), "Expected Expression"),
_ => punct!(";"), _ => punct!(";"),
(tuple_to_let(name, val)) (tuple_to_let(name, val))
) )
@ -870,9 +871,9 @@ fn tuple_to_import(tok: Token, tok2: Token) -> Statement {
make_fn!( make_fn!(
import_stmt_body<SliceIter<Token>, Statement>, import_stmt_body<SliceIter<Token>, Statement>,
do_each!( do_each!(
path => match_type!(STR), path => wrap_err!(match_type!(STR), "Expected import path"),
_ => word!("as"), _ => word!("as"),
name => match_type!(BAREWORD), name => wrap_err!(match_type!(BAREWORD), "Expected import name"),
_ => punct!(";"), _ => punct!(";"),
(tuple_to_import(path, name)) (tuple_to_import(path, name))
) )
@ -902,8 +903,8 @@ make_fn!(
out_statement<SliceIter<Token>, Statement>, out_statement<SliceIter<Token>, Statement>,
do_each!( do_each!(
_ => word!("out"), _ => word!("out"),
typ => must!(match_type!(BAREWORD)), typ => wrap_err!(must!(match_type!(BAREWORD)), "Expected converter name"),
expr => must!(expression), expr => wrap_err!(must!(expression), "Expected Expression to export"),
_ => must!(punct!(";")), _ => must!(punct!(";")),
(Statement::Output(typ.clone(), expr.clone())) (Statement::Output(typ.clone(), expr.clone()))
) )