From 77495c8d0381697e3ad2c278a1442892796b1c36 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 3 Aug 2022 01:14:01 +0300 Subject: [PATCH] all: support `assert condition, extra_message`, evaluating and showing extra_message on assert failure (#15322) --- CHANGELOG.md | 1 + cmd/tools/vast/vast.v | 4 ++++ doc/docs.md | 14 +++++++++++++ vlib/builtin/builtin.v | 6 ++++++ vlib/v/ast/ast.v | 4 +++- vlib/v/checker/checker.v | 8 ++++++++ vlib/v/checker/tests/assert_extra_message.out | 20 +++++++++++++++++++ vlib/v/checker/tests/assert_extra_message.vv | 15 ++++++++++++++ vlib/v/fmt/fmt.v | 4 ++++ vlib/v/fmt/tests/assert_extra_message_keep.vv | 6 ++++++ vlib/v/gen/c/assert.v | 15 +++++++++++++- vlib/v/markused/walker.v | 5 ++++- vlib/v/parser/parser.v | 11 ++++++++++ vlib/v/preludes/test_runner_normal.v | 9 +++++++++ ...sert_with_extra_message_works_test.run.out | 6 ++++++ ...tra_message_works_test.skip_unused.run.out | 6 ++++++ .../assert_with_extra_message_works_test.vv | 9 +++++++++ 17 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 vlib/v/checker/tests/assert_extra_message.out create mode 100644 vlib/v/checker/tests/assert_extra_message.vv create mode 100644 vlib/v/fmt/tests/assert_extra_message_keep.vv create mode 100644 vlib/v/tests/skip_unused/assert_with_extra_message_works_test.run.out create mode 100644 vlib/v/tests/skip_unused/assert_with_extra_message_works_test.skip_unused.run.out create mode 100644 vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv diff --git a/CHANGELOG.md b/CHANGELOG.md index 97913a8af..a05766ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ is made via `vgret` and is compared to the expected result. - VLS performance improvements, especially on Windows. - Add `v ls` tool for installing, for updating, and for launching VLS (V Language Server) +- Support `assert condition, extra_message`, where the `extra_message` will be evaluated and shown if the assertion fails. ## V 0.3 *30 Jun 2022* diff --git a/cmd/tools/vast/vast.v b/cmd/tools/vast/vast.v index 100ac859d..89bd9b9df 100644 --- a/cmd/tools/vast/vast.v +++ b/cmd/tools/vast/vast.v @@ -949,6 +949,10 @@ fn (t Tree) assert_stmt(node ast.AssertStmt) &Node { obj.add_terse('ast_type', t.string_node('AssertStmt')) obj.add_terse('expr', t.expr(node.expr)) obj.add_terse('is_used', t.bool_node(node.is_used)) + if node.extra !is ast.EmptyExpr { + obj.add_terse('extra', t.expr(node.extra)) + obj.add('extra_pos', t.pos(node.extra_pos)) + } obj.add('pos', t.pos(node.pos)) return obj } diff --git a/doc/docs.md b/doc/docs.md index 6282610b6..3b9326d39 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -3995,6 +3995,20 @@ assert fails it is reported to *stderr*, and the values on each side of a compar (such as `<`, `==`) will be printed when possible. This is useful to easily find an unexpected value. Assert statements can be used in any function. +### Asserts with an extra message + +This form of the `assert` statement, will print the extra message when it fails. Note, that +you can use any string expression there - string literals, functions returning a string, +strings that interpolate variables, etc. + +```v +fn test_assertion_with_extra_message_failure() { + for i in 0 .. 100 { + assert i * 2 - 45 < 75 + 10, 'assertion failed for i: $i' + } +} +``` + ### Test files ```v diff --git a/vlib/builtin/builtin.v b/vlib/builtin/builtin.v index f64e7b3c0..af647b567 100644 --- a/vlib/builtin/builtin.v +++ b/vlib/builtin/builtin.v @@ -54,6 +54,8 @@ pub: rlabel string // the right side of the infix expressions as source lvalue string // the stringified *actual value* of the left side of a failed assertion rvalue string // the stringified *actual value* of the right side of a failed assertion + message string // the value of the `message` from `assert cond, message` + has_msg bool // false for assertions like `assert cond`, true for `assert cond, 'oh no'` } // free frees the memory occupied by the assertion meta data. It is called automatically by @@ -69,6 +71,7 @@ pub fn (ami &VAssertMetaInfo) free() { ami.rlabel.free() ami.lvalue.free() ami.rvalue.free() + ami.message.free() } } @@ -81,6 +84,9 @@ fn __print_assert_failure(i &VAssertMetaInfo) { } else { eprintln(' right value: $i.rlabel = $i.rvalue') } + if i.has_msg { + eprintln(' message: $i.message') + } } } diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 699055a49..efabaef41 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1510,9 +1510,11 @@ pub const ( [minify] pub struct AssertStmt { pub: - pos token.Pos + pos token.Pos + extra_pos token.Pos pub mut: expr Expr + extra Expr is_used bool // asserts are used in _test.v files, as well as in non -prod builds of all files } diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 521d77ba4..26d46d2b3 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1612,6 +1612,14 @@ fn (mut c Checker) assert_stmt(node ast.AssertStmt) { c.error('assert can be used only with `bool` expressions, but found `$atype_name` instead', node.pos) } + if node.extra !is ast.EmptyExpr { + extra_type := c.expr(node.extra) + if extra_type != ast.string_type { + extra_type_name := c.table.sym(extra_type).name + c.error('assert allows only a single string as its second argument, but found `$extra_type_name` instead', + node.extra_pos) + } + } c.fail_if_unreadable(node.expr, ast.bool_type_idx, 'assertion') c.expected_type = cur_exp_typ } diff --git a/vlib/v/checker/tests/assert_extra_message.out b/vlib/v/checker/tests/assert_extra_message.out new file mode 100644 index 000000000..ca4e4c2e8 --- /dev/null +++ b/vlib/v/checker/tests/assert_extra_message.out @@ -0,0 +1,20 @@ +vlib/v/checker/tests/assert_extra_message.vv:12:24: error: assert allows only a single string as its second argument, but found `void` instead + 10 | assert 2 == 6 / 3, 'math works' + 11 | assert 10 == i - 120, 'i: $i' + 12 | assert 10 == i - 120, abc(i) + | ~~~~~~ + 13 | assert 10 == i - 120, multy(i) + 14 | assert 10 == i - 120, 456 +vlib/v/checker/tests/assert_extra_message.vv:13:24: error: assert allows only a single string as its second argument, but found `(int, string)` instead + 11 | assert 10 == i - 120, 'i: $i' + 12 | assert 10 == i - 120, abc(i) + 13 | assert 10 == i - 120, multy(i) + | ~~~~~~~~ + 14 | assert 10 == i - 120, 456 + 15 | } +vlib/v/checker/tests/assert_extra_message.vv:14:24: error: assert allows only a single string as its second argument, but found `int literal` instead + 12 | assert 10 == i - 120, abc(i) + 13 | assert 10 == i - 120, multy(i) + 14 | assert 10 == i - 120, 456 + | ~~~ + 15 | } diff --git a/vlib/v/checker/tests/assert_extra_message.vv b/vlib/v/checker/tests/assert_extra_message.vv new file mode 100644 index 000000000..fba664dfb --- /dev/null +++ b/vlib/v/checker/tests/assert_extra_message.vv @@ -0,0 +1,15 @@ +fn abc(i int) { +} + +fn multy(i int) (int, string) { + return i, 'aa' +} + +fn main() { + i := 123 + assert 2 == 6 / 3, 'math works' + assert 10 == i - 120, 'i: $i' + assert 10 == i - 120, abc(i) + assert 10 == i - 120, multy(i) + assert 10 == i - 120, 456 +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 3f8a36885..fc6cdf099 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -750,6 +750,10 @@ pub fn (mut f Fmt) assert_stmt(node ast.AssertStmt) { expr = (expr as ast.ParExpr).expr } f.expr(expr) + if node.extra !is ast.EmptyExpr { + f.write(', ') + f.expr(node.extra) + } f.writeln('') } diff --git a/vlib/v/fmt/tests/assert_extra_message_keep.vv b/vlib/v/fmt/tests/assert_extra_message_keep.vv new file mode 100644 index 000000000..2e3755f2e --- /dev/null +++ b/vlib/v/fmt/tests/assert_extra_message_keep.vv @@ -0,0 +1,6 @@ +fn main() { + i := 123 + assert 4 == 2 * 2 + assert 2 == 6 / 3, 'math works' + assert 10 == i - 120, 'i: $i' +} diff --git a/vlib/v/gen/c/assert.v b/vlib/v/gen/c/assert.v index dac646b5d..a1d6e94f8 100644 --- a/vlib/v/gen/c/assert.v +++ b/vlib/v/gen/c/assert.v @@ -99,7 +99,11 @@ fn (mut g Gen) gen_assert_metainfo(node ast.AssertStmt) string { mod_path := cestring(g.file.path) fn_name := g.fn_decl.name line_nr := node.pos.line_nr - src := cestring(node.expr.str()) + mut src := node.expr.str() + if node.extra !is ast.EmptyExpr { + src += ', ' + node.extra.str() + } + src = cestring(src) metaname := 'v_assert_meta_info_$g.new_tmp_var()' g.writeln('\tVAssertMetaInfo $metaname = {0};') g.writeln('\t${metaname}.fpath = ${ctoslit(mod_path)};') @@ -127,6 +131,15 @@ fn (mut g Gen) gen_assert_metainfo(node ast.AssertStmt) string { } else {} } + if node.extra is ast.EmptyExpr { + g.writeln('\t${metaname}.has_msg = false;') + g.writeln('\t${metaname}.message = _SLIT0;') + } else { + g.writeln('\t${metaname}.has_msg = true;') + g.write('\t${metaname}.message = ') + g.gen_assert_single_expr(node.extra, ast.string_type) + g.writeln(';') + } return metaname } diff --git a/vlib/v/markused/walker.v b/vlib/v/markused/walker.v index 40207c613..537c8c7e6 100644 --- a/vlib/v/markused/walker.v +++ b/vlib/v/markused/walker.v @@ -106,8 +106,11 @@ pub fn (mut w Walker) stmt(node_ ast.Stmt) { } ast.AssertStmt { if node.is_used { - w.expr(node.expr) w.n_asserts++ + w.expr(node.expr) + if node.extra !is ast.EmptyExpr { + w.expr(node.extra) + } } } ast.AssignStmt { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index fd7516225..3b3ebd695 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -877,8 +877,19 @@ pub fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { mut pos := p.tok.pos() expr := p.expr(0) pos.update_last_line(p.prev_tok.line_nr) + mut extra := ast.empty_expr + mut extra_pos := p.tok.pos() + if p.tok.kind == .comma { + p.next() + extra_pos = p.tok.pos() + extra = p.expr(0) + // dump(extra) + extra_pos = extra_pos.extend(p.tok.pos()) + } return ast.AssertStmt{ expr: expr + extra: extra + extra_pos: extra_pos pos: pos.extend(p.tok.pos()) is_used: p.inside_test_file || !p.pref.is_prod } diff --git a/vlib/v/preludes/test_runner_normal.v b/vlib/v/preludes/test_runner_normal.v index c219a50d9..d853d962d 100644 --- a/vlib/v/preludes/test_runner_normal.v +++ b/vlib/v/preludes/test_runner_normal.v @@ -154,6 +154,15 @@ fn (mut runner NormalTestRunner) assert_fail(i &VAssertMetaInfo) { } else { eprintln(' $final_src') } + if i.has_msg { + mut mtitle := ' Message:' + mut mvalue := '$i.message' + if runner.use_color { + mvalue = term.yellow(mvalue) + mtitle = term.gray(mtitle) + } + eprintln('$mtitle $mvalue') + } eprintln('') runner.all_assertsions << i } diff --git a/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.run.out b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.run.out new file mode 100644 index 000000000..550a59ba7 --- /dev/null +++ b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.run.out @@ -0,0 +1,6 @@ +vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv:7: fn test_interpolation_with_assert_that_has_extra_message + > assert 'abc$i' != 'abc77', my_failure_message(i) + Left value: abc77 + Right value: abc77 + Message: the assert failed :-|, i: 77 + diff --git a/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.skip_unused.run.out b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.skip_unused.run.out new file mode 100644 index 000000000..550a59ba7 --- /dev/null +++ b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.skip_unused.run.out @@ -0,0 +1,6 @@ +vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv:7: fn test_interpolation_with_assert_that_has_extra_message + > assert 'abc$i' != 'abc77', my_failure_message(i) + Left value: abc77 + Right value: abc77 + Message: the assert failed :-|, i: 77 + diff --git a/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv new file mode 100644 index 000000000..9098c6b73 --- /dev/null +++ b/vlib/v/tests/skip_unused/assert_with_extra_message_works_test.vv @@ -0,0 +1,9 @@ +fn my_failure_message(i int) string { + return 'the assert failed :-|, i: $i' +} + +fn test_interpolation_with_assert_that_has_extra_message() { + for i in 0 .. 100 { + assert 'abc$i' != 'abc77', my_failure_message(i) + } +} -- 2.30.2