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 |{ = }|;
assert |let foo |;
assert |let |;
assert |import |;
assert |import foo |;
assert |out |;
assert |out "|;
assert |out json|;
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 list3 = [1, 2];
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|;
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;
|;
// Deep Comparisons
let tpl4 = {
@ -40,17 +58,33 @@ let less = {
foo = "bar"
};
assert |tpl4.inner == copy.inner|;
assert |tpl4.inner.fld == copy.inner.fld|;
assert |tpl4.lst == copy.lst|;
assert |tpl4.foo == copy.foo|;
assert |tpl4 == copy|;
assert |tpl4 != extra|;
assert |tpl4 != less|;
assert |
tpl4.inner == copy.inner;
|;
assert |
tpl4.inner.fld == copy.inner.fld;
|;
assert |
tpl4.lst == copy.lst;
|;
assert |
tpl4.foo == copy.foo;
|;
assert |
tpl4 == copy;
|;
assert |
tpl4 != extra;
|;
assert |
tpl4 != less;
|;
// Expression comparisons
assert |2 == 1+1|;
assert |(1+1) == 2|;
assert |(1+1) == (1+1)|;
assert |2 == 1+1;|;
assert |(1+1) == 2;|;
assert |(1+1) == (1+1);|;
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 |[1, 2, 3] + [4, 5, 6] == [1, 2, 3, 4, 5, 6]|;
assert |
"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 = {
foo = NULL,
};
assert |tpl.foo == empty|;
assert |
tpl.foo == empty;
|;

View File

@ -1,4 +1,12 @@
assert |"hello @" % ("world") == "hello world"|;
assert |"1 @ @" % (2, 3) == "1 2 3"|;
assert |"@ or @" % (true, false) == "true or false"|;
assert |"@" % (NULL) == "NULL"|;
assert |
"hello @" % ("world") == "hello world";
|;
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,
};
assert |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 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 |filter filtrator.result list2 == ["foo", "foo"]|;
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]|;
assert |
filter filtrator.result list2 == ["foo", "foo"];
|;
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 cplxresult = cplxmacro(1, "We", 3.0);
assert |simpleresult.field1 == 1|;
assert |simpleresult.field2 == 2|;
assert |simpleresult.field3 == 3|;
assert |
simpleresult.field1 == 1;
|;
assert |
simpleresult.field2 == 2;
|;
assert |
simpleresult.field3 == 3;
|;
assert |cplxresult.field1 == 2|;
assert |cplxresult.field2 == "We are here"|;
assert |cplxresult.field3 == 2.0|;
assert |cplxresult.boolfield == true|;
assert |
cplxresult.field1 == 2;
|;
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 |2 + 2 * 3 == 8|;
assert |2 * (2 + 1) == 6|;
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|;
assert |
2 * 2 + 1 == 5;
|;
assert |
2 + 2 * 3 == 8;
|;
assert |
2 * (2 + 1) == 6;
|;
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",
};
assert |got == "grand prize"|;
assert |defaultgot == "OOPS"|;
assert |
got == "grand prize";
|;
assert |
defaultgot == "OOPS";
|;
// select inside a macro
@ -25,6 +29,12 @@ let condmacro = macro(arg) => {
let result = condmacro("opt1");
assert |condmacro("opt1") == {output = "yay"}|;
assert |condmacro("opt2") == {output = "boo"}|;
assert |condmacro("invalid") == {output = NULL}|;
assert |
condmacro("opt1") == {output = "yay"};
|;
assert |
condmacro("opt2") == {output = "boo"};
|;
assert |
condmacro("invalid") == {output = NULL};
|;

View File

@ -10,10 +10,24 @@ let testmacro = macro(arg) => {
output = arg,
};
assert |list.0 == 1|;
assert |list.1 == 2|;
assert |list.3 == 4|;
assert |tuple.field1 == 1|;
assert |tuple.field2 == 3|;
assert |tuple.deeplist.0 == "foo"|;
assert |tuple.deeplist.1 == "bar"|;
assert |
list.0 == 1;
|;
assert |
list.1 == 2;
|;
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],
};
assert |simpletpl.foo == "bar"|;
assert |stringfieldtpl."field 1" == 1|;
assert |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|;
assert |
simpletpl.foo == "bar";
|;
assert |
stringfieldtpl."field 1" == 1;
|;
assert |
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::string::ToString;
use simple_error;
use ast::*;
use error;
use format;
@ -248,7 +250,7 @@ impl<'a> Builder<'a> {
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()) {
Ok(stmts) => {
//panic!("Successfully parsed {}", input);
@ -271,7 +273,7 @@ impl<'a> Builder<'a> {
/// Evaluate an input string as UCG.
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.
@ -280,8 +282,19 @@ impl<'a> Builder<'a> {
let mut f = try!(File::open(name));
let mut s = String::new();
try!(f.read_to_string(&mut s));
self.last = Some(try!(self.eval_string(&s)));
Ok(())
let eval_result = self.eval_string(&s);
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>> {
@ -993,13 +1006,10 @@ impl<'a> Builder<'a> {
// we are not in validate_mode then build_asserts are noops.
return Ok(Rc::new(Val::Empty));
}
let mut expr_as_stmt = String::new();
let expr = &tok.fragment;
expr_as_stmt.push_str(expr);
expr_as_stmt.push_str(";");
let assert_input =
OffsetStrIter::new_with_offsets(&expr_as_stmt, tok.pos.line - 1, tok.pos.column - 1);
let ok = match self.eval_span(assert_input) {
OffsetStrIter::new_with_offsets(expr, tok.pos.line - 1, tok.pos.column - 1);
let ok = match self.eval_input(assert_input) {
Ok(v) => v,
Err(e) => {
// 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
//! 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
//! delimited by `|` characters.
//! of unit testing for your configurations. It starts with the assert keyword followed by a valid block of ucg statements
//! delimited by `|` characters. The final statement in the in the block must evaluate to a boolean expression.
//!
//! ```ucg
//! assert host == "www.example.com";
//! assert |select qa, 443, {
//! qa = 80,
//! prod = 443,
//! } == 443|;
//! assert |
//! host == "www.example.com";
//! |;
//! assert |
//! 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

View File

@ -843,9 +843,10 @@ fn tuple_to_let(tok: Token, expr: Expression) -> Statement {
make_fn!(
let_stmt_body<SliceIter<Token>, Statement>,
do_each!(
name => match_type!(BAREWORD),
name => wrap_err!(match_type!(BAREWORD), "Expected name for binding"),
_ => 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!(";"),
(tuple_to_let(name, val))
)
@ -870,9 +871,9 @@ fn tuple_to_import(tok: Token, tok2: Token) -> Statement {
make_fn!(
import_stmt_body<SliceIter<Token>, Statement>,
do_each!(
path => match_type!(STR),
path => wrap_err!(match_type!(STR), "Expected import path"),
_ => word!("as"),
name => match_type!(BAREWORD),
name => wrap_err!(match_type!(BAREWORD), "Expected import name"),
_ => punct!(";"),
(tuple_to_import(path, name))
)
@ -902,8 +903,8 @@ make_fn!(
out_statement<SliceIter<Token>, Statement>,
do_each!(
_ => word!("out"),
typ => must!(match_type!(BAREWORD)),
expr => must!(expression),
typ => wrap_err!(must!(match_type!(BAREWORD)), "Expected converter name"),
expr => wrap_err!(must!(expression), "Expected Expression to export"),
_ => must!(punct!(";")),
(Statement::Output(typ.clone(), expr.clone()))
)