diff --git a/README.md b/README.md index 369d025..e47f5fe 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,9 @@ per file, prints `PASS`/`FAIL` per file, and exits non-zero when any test fails. A project only needs `package.fun` (a `bin` entry is not required); manifest dependencies are available through the same launcher as `fun run`. -- Assertions: `assert.equal`, `not_equal`, `is_true`, `is_false`, `raises`, - `fail`; each takes an optional trailing message. +- Assertions: `assert.equal`, `not_equal`, `is_true`, `is_false`, `match`, + `contains`, `greater`, `less`, `is_nil`, `not_nil`, `raises`, `fail`; each + takes an optional trailing message. - `test.fun`, the globals `test`/`assert`/`runTests`, the aliases `as test` / `as assert`, and dependency keys `test`/`assert` are reserved. A third-party test library can be imported under another alias. diff --git a/README.zh.md b/README.zh.md index fc657ab..3aa04d9 100644 --- a/README.zh.md +++ b/README.zh.md @@ -184,8 +184,9 @@ fun test `package.fun`(不要求 `bin`);清单里的依赖通过和 `fun run` 相同的项目 启动器生效。 -- 断言:`assert.equal`、`not_equal`、`is_true`、`is_false`、`raises`、`fail`, - 每个都可带最后一个自定义消息参数。 +- 断言:`assert.equal`、`not_equal`、`is_true`、`is_false`、`match`、`contains`、 + `greater`、`less`、`is_nil`、`not_nil`、`raises`、`fail`,每个都可带最后一个 + 自定义消息参数。 - `test.fun`、全局名 `test`/`assert`/`runTests`、别名 `as test`/`as assert`、 依赖键 `test`/`assert` 为保留名;三方测试库请换别名导入。 - `dist/`、`.` 开头目录和 path 依赖目录不会参与发现。 diff --git a/fun/lib/test.fun b/fun/lib/test.fun index d754e5f..5197052 100644 --- a/fun/lib/test.fun +++ b/fun/lib/test.fun @@ -5,6 +5,9 @@ // // Test files import `test.fun` and register blocks: // test('name', { assert.equal(1 + 1, 2); }); +// Assertions: equal, not_equal, is_true, is_false, match (regex), contains +// (substring), greater, less, is_nil, not_nil, raises, fail; each takes an +// optional trailing message. // The runner calls `runTests()`; each test file runs in its own process. var _tests = new []; var _current = ''; @@ -58,6 +61,42 @@ fun assert_false(cond, msg) end if; end fun; +fun assert_match(s, re, msg) + if not (s =~ re) then + _fail(_compose(msg, "expected a match for '" & _str(s) & "'")); + end if; +end fun; + +fun assert_contains(s, part, msg) + if s.subpos(part) < 0 then + _fail(_compose(msg, "expected to contain '" & _str(part) & "', got '" & _str(s) & "'")); + end if; +end fun; + +fun assert_greater(actual, limit, msg) + if not (actual > limit) then + _fail(_compose(msg, 'expected > ' & _str(limit) & ', got ' & _str(actual))); + end if; +end fun; + +fun assert_less(actual, limit, msg) + if not (actual < limit) then + _fail(_compose(msg, 'expected < ' & _str(limit) & ', got ' & _str(actual))); + end if; +end fun; + +fun assert_is_nil(v, msg) + if v <> nil then + _fail(_compose(msg, 'expected nil, got ' & _str(v))); + end if; +end fun; + +fun assert_not_nil(v, msg) + if v = nil then + _fail(_compose(msg, 'expected not nil')); + end if; +end fun; + fun assert_raises(block, part, msg) var raised = false; var caught = ''; @@ -83,6 +122,12 @@ var assert = [ not_equal: assert_not_equal, is_true: assert_true, is_false: assert_false, + match: assert_match, + contains: assert_contains, + greater: assert_greater, + less: assert_less, + is_nil: assert_is_nil, + not_nil: assert_not_nil, raises: assert_raises, fail: assert_fail ]; diff --git a/fun/test/expected/lib-fun-test-test.fun.txt b/fun/test/expected/lib-fun-test-test.fun.txt index 7b31a0a..71e8d41 100644 --- a/fun/test/expected/lib-fun-test-test.fun.txt +++ b/fun/test/expected/lib-fun-test-test.fun.txt @@ -14,6 +14,7 @@ crash marked FAIL: True custom message shown: True diagnostic shown: True assert.fail shown: True +matcher failure shown: True passing test quiet: True failure summary: True reserved dependency key: True diff --git a/fun/test/lib-fun-test-test.fun b/fun/test/lib-fun-test-test.fun index 06ed3e0..23d0c2a 100644 --- a/fun/test/lib-fun-test-test.fun +++ b/fun/test/lib-fun-test-test.fun @@ -132,6 +132,14 @@ var proj = base & 'main proj' & sep; "test('raises and is_false', {" & nl & " assert.raises({ assert.fail('boom inside'); }, 'boom inside');" & nl & ' assert.is_false(2 < 1);' & nl & + '});' & nl & nl & + "test('matchers', {" & nl & + " assert.match('hello world', /^hello/);" & nl & + " assert.contains('hello world', 'lo wo');" & nl & + ' assert.greater(3, 2);' & nl & + ' assert.less(2, 3);' & nl & + ' assert.is_nil(nil);' & nl & + " assert.not_nil('x');" & nl & '});' & nl); (proj & 'tests' & sep & 'nested' & sep & 'deep_test.fun').save( "use 'test.fun';" & nl & @@ -172,12 +180,12 @@ var mo = loadIf(mlog); check('test exits 0', m1 = 0); check('smoke found', has(mo, 'smoke_test.fun') and has(mo, 'PASS (1)')); check('src test found', has(mo, 'src/b_test.fun')); -check('tests helper reuse', has(mo, 'tests/a_test.fun') and has(mo, 'PASS (3)')); +check('tests helper reuse', has(mo, 'tests/a_test.fun') and has(mo, 'PASS (4)')); check('nested test found', has(mo, 'tests/nested/deep_test.fun')); check('dist excluded', not has(mo, 'skip_test.fun')); check('path dep excluded', not has(mo, 'dep_test.fun')); check('helper not a test', not has(mo, 'helpers.fun')); -check('summary', has(mo, '4 files, 6 tests: all passed')); +check('summary', has(mo, '4 files, 7 tests: all passed')); // --- lib-only package (no bin) ------------------------------------------- var libp = base & 'libonly' & sep; @@ -221,6 +229,9 @@ var failp = base & 'failing' & sep; '});' & nl & nl & "test('explicit fail', {" & nl & " assert.fail('unexpected state');" & nl & + '});' & nl & nl & + "test('matcher failure', {" & nl & + " assert.match('abc', /xyz/, 'must match');" & nl & '});' & nl); (failp & 'crash_test.fun').save( "use 'test.fun';" & nl & @@ -234,8 +245,9 @@ check('crash marked FAIL', has(fo, 'crash_test.fun') and has(fo, 'boom at load') check('custom message shown', has(fo, 'not ok custom message') and has(fo, 'math is broken')); check('diagnostic shown', has(fo, 'expected 3, got 2')); check('assert.fail shown', has(fo, 'not ok explicit fail') and has(fo, 'unexpected state')); +check('matcher failure shown', has(fo, 'not ok matcher failure') and has(fo, 'must match')); check('passing test quiet', not has(fo, 'ok passing')); -check('failure summary', has(fo, '2 files, 3 tests: 1 passed, 2 failed; 1 file failed to run')); +check('failure summary', has(fo, '2 files, 4 tests: 1 passed, 3 failed; 1 file failed to run')); // --- reserved names and shadowing ---------------------------------------- var resp = base & 'reserved' & sep;