mt-lang 0.4.2 → 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/docs/index.html +5 -0
- data/docs/language-manual.md +8 -1
- data/docs/testing.md +91 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/control_flow/builder.rb +10 -1
- data/lib/milk_tea/core/lowering/assertions.rb +100 -0
- data/lib/milk_tea/core/lowering/block.rb +5 -0
- data/lib/milk_tea/core/lowering/calls.rb +2 -0
- data/lib/milk_tea/core/lowering/resolve.rb +4 -0
- data/lib/milk_tea/core/lowering.rb +2 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +31 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +8 -0
- data/lib/milk_tea/tooling/cli/commands/test.rb +39 -40
- data/lib/milk_tea/tooling/cli.rb +5 -4
- data/lib/milk_tea/tooling/linter/rules.rb +13 -2
- data/lib/milk_tea/tooling/linter/visitors.rb +17 -0
- data/lib/milk_tea/tooling/linter.rb +4 -3
- metadata +4 -3
- data/std/testing.mt +0 -266
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60f503669467624ebec5461f2f5fb5c3dae9216718b4c445f182323b90e9f505
|
|
4
|
+
data.tar.gz: d5ace59e3fdadbe3a7ea75f740d0d1d2953088d1799b42e9e6899c900e50595a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e74f76c097577a41ccc21462f7ede0d2c2a77f84f883e0ca3026b6b3f966e4b87ba3cfc91eca5dc16165edbc50a6f2bcff6be7ef6be751a1ba95f86b472b6e00
|
|
7
|
+
data.tar.gz: 652e7de9f0afd01aeac1358af1c47a2a19bce244ec53838b5cfc65b103d44cafea61210d01ca766bf390d4b649268d50d6ac2c057ce40e09f64e25b24c4a3d76
|
data/README.md
CHANGED
|
@@ -20,7 +20,7 @@ It is the preferred first entry point for getting to know the language.
|
|
|
20
20
|
If this file conflicts with `docs/language-manual.md`, the manual wins.
|
|
21
21
|
`docs/language-design.md` is for design direction and rationale, not the authoritative implementation spec.
|
|
22
22
|
|
|
23
|
-
Package manifests, build workflow, and run workflow are documented separately in `docs/build-guide.md`.
|
|
23
|
+
Package manifests, build workflow, and run workflow are documented separately in `docs/build-guide.md`. The `@[test]` attribute, assertion intrinsics, and `mtc test` runner are documented in `docs/testing.md`.
|
|
24
24
|
|
|
25
25
|
## 1. File Kinds And Layout
|
|
26
26
|
|
|
@@ -860,6 +860,10 @@ Generics:
|
|
|
860
860
|
Special recognized callables:
|
|
861
861
|
|
|
862
862
|
- `fatal(message)`
|
|
863
|
+
- `assert(condition, message?)` — runtime check; aborts when the `bool` condition is false
|
|
864
|
+
- `expect(condition, message?)` — same as `assert`, always-on test assertion
|
|
865
|
+
- `expect_eq(actual, expected, message?)` — compares with the language `==` operator (primitives, `str`, structs, variants, arrays) and aborts when the values differ
|
|
866
|
+
- `expect_ne(actual, expected, message?)` — compares with `!=` and aborts when the values are equal
|
|
863
867
|
- `ref_of(x)`
|
|
864
868
|
- `const_ptr_of(x)`
|
|
865
869
|
- `read(r)`
|
|
@@ -877,6 +881,8 @@ Special recognized callables:
|
|
|
877
881
|
- `get(coll, index)` — recoverable array/span indexing returning `ptr[T]?`; null on out‑of‑bounds instead of aborting
|
|
878
882
|
- `adapt[I](value)` — constructs a `dyn[I]` runtime interface value; verifies `value`'s type implements interface `I` at compile time
|
|
879
883
|
|
|
884
|
+
`assert`, `expect`, `expect_eq`, and `expect_ne` are recognized in bare call position (not reserved words) and take an optional `str`/`cstr` message. When omitted, the compiler synthesizes a message carrying the source location; the message is only evaluated on the failing path. A literal-false `assert(false, ...)` / `expect(false, ...)` is treated as terminating control flow, so it satisfies the `else:` block of a `let ... else:` guard.
|
|
885
|
+
|
|
880
886
|
Reference and pointer notes:
|
|
881
887
|
|
|
882
888
|
- `read(ref_value)` explicitly projects the referent value. Use `read(handle) = value` to write through a bare `ref[T]` value.
|
data/docs/index.html
CHANGED
|
@@ -1879,6 +1879,10 @@ let sized = name[32]</code></pre>
|
|
|
1879
1879
|
<table class="attr-table">
|
|
1880
1880
|
<tr><th>Callable</th><th>Description</th></tr>
|
|
1881
1881
|
<tr><td><code>fatal(message)</code></td><td>Terminate with message</td></tr>
|
|
1882
|
+
<tr><td><code>assert(condition, message?)</code></td><td>Runtime check; aborts when the <code>bool</code> condition is false</td></tr>
|
|
1883
|
+
<tr><td><code>expect(condition, message?)</code></td><td>Always-on test assertion (same as <code>assert</code>)</td></tr>
|
|
1884
|
+
<tr><td><code>expect_eq(actual, expected, message?)</code></td><td>Compares with the language <code>==</code> (primitives, <code>str</code>, structs, variants, arrays); aborts when the values differ</td></tr>
|
|
1885
|
+
<tr><td><code>expect_ne(actual, expected, message?)</code></td><td>Compares with <code>!=</code>; aborts when the values are equal</td></tr>
|
|
1882
1886
|
<tr><td><code>ref_of(x)</code></td><td>Writable safe reference to an addressable lvalue</td></tr>
|
|
1883
1887
|
<tr><td><code>const_ptr_of(x)</code></td><td>Read-only raw pointer to an addressable lvalue</td></tr>
|
|
1884
1888
|
<tr><td><code>ptr_of(x)</code></td><td>Writable raw pointer from a mutable addressable lvalue</td></tr>
|
|
@@ -2660,6 +2664,7 @@ const ATOMS = new Set(['true','false','null'])
|
|
|
2660
2664
|
const BUILTINS = new Set([
|
|
2661
2665
|
'ref_of','ptr_of','const_ptr_of','read','reinterpret','zero','default',
|
|
2662
2666
|
'hash','equal','order','size_of','align_of','offset_of','get','adapt','fatal',
|
|
2667
|
+
'assert','expect','expect_eq','expect_ne',
|
|
2663
2668
|
'field_of','callable_of','attribute_of','has_attribute','attribute_arg',
|
|
2664
2669
|
'fields_of','members_of','attributes_of',
|
|
2665
2670
|
])
|
data/docs/language-manual.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This manual documents the Milk Tea language as implemented today in the lexer, parser, semantic checker, and compiler tests.
|
|
4
4
|
|
|
5
|
-
Package manifests and build or run workflow are documented separately in `docs/build-guide.md`.
|
|
5
|
+
Package manifests and build or run workflow are documented separately in `docs/build-guide.md`. The `@[test]` attribute, assertion intrinsics, and `mtc test` runner are documented in `docs/testing.md`.
|
|
6
6
|
|
|
7
7
|
## 1. Source Files And Modules
|
|
8
8
|
|
|
@@ -1225,6 +1225,10 @@ The call site specializes with a literal: `int_with_bits[64]`.
|
|
|
1225
1225
|
Special recognized callables:
|
|
1226
1226
|
|
|
1227
1227
|
- `fatal(message)`
|
|
1228
|
+
- `assert(condition, message?)` — runtime check; aborts with `message` when the `bool` condition is false
|
|
1229
|
+
- `expect(condition, message?)` — same as `assert`, always-on test assertion
|
|
1230
|
+
- `expect_eq(actual, expected, message?)` — compares with the language `==` operator (works for primitives, `str`, structs, variants, arrays) and aborts when the values differ
|
|
1231
|
+
- `expect_ne(actual, expected, message?)` — compares with `!=` and aborts when the values are equal
|
|
1228
1232
|
- `ref_of(x)`
|
|
1229
1233
|
- `const_ptr_of(x)`
|
|
1230
1234
|
- `read(r)`
|
|
@@ -1242,6 +1246,8 @@ Special recognized callables:
|
|
|
1242
1246
|
- `get(coll, index)` — recoverable array/span indexing returning `ptr[T]?`; null on out‑of‑bounds instead of aborting
|
|
1243
1247
|
- `adapt[I](value)` — constructs a `dyn[I]` runtime interface value; verifies `value`'s type implements `I` at compile time
|
|
1244
1248
|
|
|
1249
|
+
`assert`, `expect`, `expect_eq`, and `expect_ne` are special recognized callables, not reserved words: they are recognized in bare call position and may be shadowed by user declarations like any ordinary name. Their message argument (when given) must be `str` or `cstr`; when omitted, the compiler synthesizes a message carrying the source location. The message is only evaluated on the failing path. A literal-false `assert(false, ...)` or `expect(false, ...)` is treated as terminating control flow (like `static_assert(false, ...)`), so it may be used where the checker requires guaranteed exit, such as the `else:` block of a `let ... else:` guard.
|
|
1250
|
+
|
|
1245
1251
|
`default[T]` requires an accessible zero-argument associated function `T.default()` that returns `T`.
|
|
1246
1252
|
|
|
1247
1253
|
`hash[T](value)` lowers to `T.hash(value: const_ptr[T]) -> uint`, `equal[T](left, right)` lowers to `T.equal(left: const_ptr[T], right: const_ptr[T]) -> bool`, and `order[T](left, right)` lowers to `T.order(left: const_ptr[T], right: const_ptr[T]) -> int`. Each argument must already be a `ref[T]`, `ptr[T]`, or `const_ptr[T]`, or be a safe stored `T` lvalue that can be borrowed implicitly.
|
|
@@ -1547,6 +1553,7 @@ The auto-fix column corresponds to `mtc lint --fix`.
|
|
|
1547
1553
|
| `shadow` | warning | — | Local binding shadows an outer binding with the same name |
|
|
1548
1554
|
| `trailing-list-comma` | hint | yes | Trailing comma in call argument list is redundant |
|
|
1549
1555
|
| `unreachable-code` | warning | — | Code after a guaranteed terminator cannot execute |
|
|
1556
|
+
| `redundant-unsafe` | hint | — | `unsafe` block contains no unsafe operations and can be removed |
|
|
1550
1557
|
| `unused-import` | warning | yes | Import alias is never referenced |
|
|
1551
1558
|
| `unused-local` | warning | — | Local binding is never referenced |
|
|
1552
1559
|
| `unused-param` | warning | — | Parameter is never referenced |
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Milk Tea Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide documents the in-language testing surface: the `assert`/`expect`/`expect_eq`/`expect_ne` intrinsics, the `@[test]` attribute, and the `mtc test` runner. The assertion intrinsics are part of the built-in callable surface (see `language-manual.md` §7); there is no standard-library testing module to import.
|
|
4
|
+
|
|
5
|
+
## 1. Assertions
|
|
6
|
+
|
|
7
|
+
Four abort-based assertion intrinsics are available in every module, in both tests and ordinary code:
|
|
8
|
+
|
|
9
|
+
```mt
|
|
10
|
+
assert(condition, message?) # runtime check
|
|
11
|
+
expect(condition, message?) # always-on test assertion
|
|
12
|
+
expect_eq(actual, expected, message?) # compares with the language `==`
|
|
13
|
+
expect_ne(actual, expected, message?) # compares with `!=`
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
|
|
18
|
+
- `condition` must be `bool`. `expect_eq`/`expect_ne` compare with the language `==`/`!=` operators, so they work for primitives, `str`, structs, variants, and arrays.
|
|
19
|
+
- `message` (when given) must be `str` or `cstr`; a `f"..."` format string flows straight in. When omitted, the compiler synthesizes a message carrying the source location.
|
|
20
|
+
- On failure the program aborts through the same path as `fatal`. The message is only evaluated on the failing path, so a `f"..."` message costs nothing while the assertion holds.
|
|
21
|
+
- `assert` defaults to always-on. `expect` is the test-flavored form and is always on.
|
|
22
|
+
- A literal-false `assert(false, ...)` or `expect(false, ...)` is treated as terminating control flow (like `static_assert(false, ...)`), so it satisfies contexts that require guaranteed exit, such as the `else:` block of a `let ... else:` guard.
|
|
23
|
+
|
|
24
|
+
## 2. Tests
|
|
25
|
+
|
|
26
|
+
Tests are ordinary functions annotated with `@[test]`. They take no parameters and return `void`; a failing assertion aborts the test.
|
|
27
|
+
|
|
28
|
+
```mt
|
|
29
|
+
function square(x: int) -> int:
|
|
30
|
+
return x * x
|
|
31
|
+
|
|
32
|
+
@[test]
|
|
33
|
+
function test_square() -> void:
|
|
34
|
+
expect_eq(square(3), 9)
|
|
35
|
+
expect(square(4) > 0, "square must be positive")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Rules:
|
|
39
|
+
|
|
40
|
+
- A test file must not define `main`; `mtc test` synthesizes the entry point.
|
|
41
|
+
- A function annotated with `@[test] @[expect_fatal]` is a death test: it must abort (via `fatal`, a failed assertion, or a failed safety check) to pass.
|
|
42
|
+
|
|
43
|
+
## 3. Running Tests
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
mtc test PATH # run @[test] functions in one file
|
|
47
|
+
mtc test DIR # recursively run every test file under a directory
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`mtc test` builds one runner binary per test file and runs each test in its own process, so an aborting assertion is isolated to its own test and cannot suppress the results of its siblings. Passing tests print `ok - name`; failures print `FAIL - name: message`. The run is sandboxed with a wall-clock timeout (default 30s) and an address-space memory cap (default 1024 MB).
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
|
|
54
|
+
- `--timeout SECONDS` — per-test wall-clock timeout
|
|
55
|
+
- `--mem MB` — per-test address-space memory cap
|
|
56
|
+
- `--jobs N` — build and run N files in parallel (output stays in file order)
|
|
57
|
+
- `-n SUBSTRING` — run only tests whose name contains SUBSTRING
|
|
58
|
+
- `--format tap|junit` — machine-readable results for CI
|
|
59
|
+
- `--sanitize` — build with AddressSanitizer/UBSan; any sanitizer error fails the run
|
|
60
|
+
- `--profile debug|release`, `--platform linux|windows|wasm`, `--cc COMPILER` — build controls, same as `mtc build`
|
|
61
|
+
|
|
62
|
+
A file containing `# expect-error: <text>` is a compile-fail fixture: `mtc test` requires the compiler to reject it with a diagnostic containing that text, instead of running it.
|
|
63
|
+
|
|
64
|
+
## 4. Example
|
|
65
|
+
|
|
66
|
+
```mt
|
|
67
|
+
# tests/vec_test.mt
|
|
68
|
+
import std.vec as vec
|
|
69
|
+
|
|
70
|
+
@[test]
|
|
71
|
+
function test_vec_push_and_len() -> void:
|
|
72
|
+
var values = vec.Vec[int].create()
|
|
73
|
+
defer: values.release()
|
|
74
|
+
values.push(1)
|
|
75
|
+
values.push(2)
|
|
76
|
+
expect_eq(values.len(), 2)
|
|
77
|
+
|
|
78
|
+
@[test]
|
|
79
|
+
@[expect_fatal]
|
|
80
|
+
function test_vec_oob_aborts() -> void:
|
|
81
|
+
var values = vec.Vec[int].create()
|
|
82
|
+
defer: values.release()
|
|
83
|
+
values.push(1)
|
|
84
|
+
values.at(5)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
mtc test tests
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See the `mtc test --help` text for the current full option list.
|
data/lib/milk_tea/base.rb
CHANGED
|
@@ -254,7 +254,16 @@ module MilkTea
|
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
def fatal_expression?(expression)
|
|
257
|
-
expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && expression.callee.name == "fatal"
|
|
257
|
+
return true if expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && expression.callee.name == "fatal"
|
|
258
|
+
|
|
259
|
+
# A literal-false `assert`/`expect` always aborts, so it terminates the
|
|
260
|
+
# current control flow like a `fatal` call (matching static_assert(false)).
|
|
261
|
+
expression.is_a?(AST::Call) &&
|
|
262
|
+
expression.callee.is_a?(AST::Identifier) &&
|
|
263
|
+
%w[assert expect].include?(expression.callee.name) &&
|
|
264
|
+
(first_arg = expression.arguments.first) &&
|
|
265
|
+
first_arg.value.is_a?(AST::BooleanLiteral) &&
|
|
266
|
+
first_arg.value.value == false
|
|
258
267
|
end
|
|
259
268
|
|
|
260
269
|
def assignment_target_reads(target, operator)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
module Lowering
|
|
5
|
+
module Assertions
|
|
6
|
+
ASSERT_CALL_NAMES = %w[assert expect expect_eq expect_ne].freeze
|
|
7
|
+
|
|
8
|
+
# Lowers an `assert`/`expect`/`expect_eq`/`expect_ne` expression statement
|
|
9
|
+
# into an `IR::IfStmt` whose then-branch aborts via `fatal`. The message is
|
|
10
|
+
# only evaluated on the failing path so unused messages cost nothing.
|
|
11
|
+
# Returns `nil` when the statement is not one of the assertion calls so the
|
|
12
|
+
# caller can fall through to ordinary expression-statement lowering.
|
|
13
|
+
def lower_assert_like_statement(statement, env:)
|
|
14
|
+
kind = assert_like_call_kind(statement.expression)
|
|
15
|
+
return nil unless kind
|
|
16
|
+
|
|
17
|
+
arguments = statement.expression.arguments
|
|
18
|
+
line = statement.expression.line
|
|
19
|
+
column = statement.expression.column
|
|
20
|
+
default_message = default_assert_message(kind, statement.line)
|
|
21
|
+
|
|
22
|
+
condition_ast = nil
|
|
23
|
+
message = nil
|
|
24
|
+
case kind
|
|
25
|
+
when "assert", "expect"
|
|
26
|
+
condition_ast = unary_expression("not", arguments.fetch(0).value, line:, column:)
|
|
27
|
+
message = arguments.length > 1 ? arguments.fetch(1).value : string_literal(default_message, line:, column:)
|
|
28
|
+
when "expect_eq"
|
|
29
|
+
condition_ast = binary_expression("!=", arguments.fetch(0).value, arguments.fetch(1).value, line:, column:)
|
|
30
|
+
message = arguments.length > 2 ? arguments.fetch(2).value : string_literal(default_message, line:, column:)
|
|
31
|
+
when "expect_ne"
|
|
32
|
+
condition_ast = binary_expression("==", arguments.fetch(0).value, arguments.fetch(1).value, line:, column:)
|
|
33
|
+
message = arguments.length > 2 ? arguments.fetch(2).value : string_literal(default_message, line:, column:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Hoist inline-proc/foreign-temporary setup out of the condition so the
|
|
37
|
+
# failure check itself stays a plain `if (!cond)` in C.
|
|
38
|
+
setup, prepared_condition, cleanups = prepare_expression_with_cleanups(
|
|
39
|
+
condition_ast,
|
|
40
|
+
env:,
|
|
41
|
+
expected_type: @ctx.types.fetch("bool"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
[
|
|
45
|
+
*setup,
|
|
46
|
+
IR::IfStmt.new(
|
|
47
|
+
condition: lower_expression(prepared_condition, env:, expected_type: @ctx.types.fetch("bool")),
|
|
48
|
+
then_body: [lower_fatal_expression_statement(message, line:, column:, env:)],
|
|
49
|
+
else_body: [],
|
|
50
|
+
),
|
|
51
|
+
*cleanups.flat_map(&:itself),
|
|
52
|
+
]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def assert_like_call_kind(expression)
|
|
56
|
+
return nil unless expression.is_a?(AST::Call)
|
|
57
|
+
return nil unless expression.callee.is_a?(AST::Identifier)
|
|
58
|
+
return nil unless ASSERT_CALL_NAMES.include?(expression.callee.name)
|
|
59
|
+
|
|
60
|
+
expression.callee.name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def default_assert_message(kind, line)
|
|
64
|
+
path = @ctx.current_analysis_path.to_s
|
|
65
|
+
case kind
|
|
66
|
+
when "assert" then "assertion failed at #{path}:#{line}"
|
|
67
|
+
when "expect" then "expectation failed at #{path}:#{line}"
|
|
68
|
+
when "expect_eq" then "expect_eq failed: values are not equal at #{path}:#{line}"
|
|
69
|
+
when "expect_ne" then "expect_ne failed: values are equal at #{path}:#{line}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def lower_fatal_expression_statement(message, line:, column:, env:)
|
|
74
|
+
fatal_call = AST::Call.new(
|
|
75
|
+
callee: AST::Identifier.new(name: "fatal", line:, column:),
|
|
76
|
+
arguments: [AST::Argument.new(name: nil, value: message, line:, column:)],
|
|
77
|
+
line:,
|
|
78
|
+
column:,
|
|
79
|
+
)
|
|
80
|
+
IR::ExpressionStmt.new(
|
|
81
|
+
expression: lower_expression(fatal_call, env:, expected_type: @ctx.types.fetch("void")),
|
|
82
|
+
line:,
|
|
83
|
+
path: @ctx.current_analysis_path,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unary_expression(operator, operand, line:, column:)
|
|
88
|
+
AST::UnaryOp.new(operator:, operand:, line:, column:)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def binary_expression(operator, left, right, line:, column:)
|
|
92
|
+
AST::BinaryOp.new(operator:, left:, right:, line:, column:)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def string_literal(value, line:, column:)
|
|
96
|
+
AST::StringLiteral.new(lexeme: value.inspect, value:, cstring: false, line:, column:)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -422,6 +422,11 @@ module MilkTea
|
|
|
422
422
|
end
|
|
423
423
|
|
|
424
424
|
def lower_expression_stmt(statement, lowered:, local_defers:, local_env:)
|
|
425
|
+
if (assert_statements = lower_assert_like_statement(statement, env: local_env))
|
|
426
|
+
lowered.concat(assert_statements)
|
|
427
|
+
return
|
|
428
|
+
end
|
|
429
|
+
|
|
425
430
|
if (format_sink_statements = lower_explicit_format_sink_expression_statement(statement.expression, env: local_env, line: statement.line))
|
|
426
431
|
lowered.concat(format_sink_statements)
|
|
427
432
|
return
|
|
@@ -271,6 +271,8 @@ module MilkTea
|
|
|
271
271
|
)
|
|
272
272
|
when :zero
|
|
273
273
|
IR::ZeroInit.new(type:)
|
|
274
|
+
when :assert, :expect, :expect_eq, :expect_ne
|
|
275
|
+
raise LoweringError.new("#{kind} must be used as a statement", line: 0, column: 0, path: @ctx.current_analysis_path)
|
|
274
276
|
when :fatal
|
|
275
277
|
argument = expression.arguments.fetch(0)
|
|
276
278
|
message_type = infer_expression_type(argument.value, env:)
|
|
@@ -6,6 +6,10 @@ module MilkTea
|
|
|
6
6
|
include CompileTime::MethodFolding
|
|
7
7
|
PASS_THROUGH_BUILTINS = {
|
|
8
8
|
"fatal" => :fatal,
|
|
9
|
+
"assert" => :assert,
|
|
10
|
+
"expect" => :expect,
|
|
11
|
+
"expect_eq" => :expect_eq,
|
|
12
|
+
"expect_ne" => :expect_ne,
|
|
9
13
|
"ref_of" => :ref_of,
|
|
10
14
|
"const_ptr_of" => :const_ptr_of,
|
|
11
15
|
"read" => :read,
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
require_relative "lowering/scans"
|
|
28
28
|
require_relative "lowering/declarations"
|
|
29
|
+
require_relative "lowering/assertions"
|
|
29
30
|
require_relative "lowering/events"
|
|
30
31
|
require_relative "lowering/functions"
|
|
31
32
|
require_relative "lowering/async/analysis"
|
|
@@ -107,6 +108,7 @@ module MilkTea
|
|
|
107
108
|
include Lowering::Functions
|
|
108
109
|
include Lowering::Async
|
|
109
110
|
include Lowering::Block
|
|
111
|
+
include Lowering::Assertions
|
|
110
112
|
include Lowering::Proc
|
|
111
113
|
include Lowering::Loops
|
|
112
114
|
include Lowering::Expressions
|
|
@@ -834,6 +834,37 @@ module MilkTea
|
|
|
834
834
|
|
|
835
835
|
raise_sema_error("fatal expects str or cstr, got #{message_type}")
|
|
836
836
|
end
|
|
837
|
+
def check_assert_call(kind, arguments, scopes:)
|
|
838
|
+
name = kind.to_s
|
|
839
|
+
raise_sema_error("#{name} does not support named arguments") if arguments.any?(&:name)
|
|
840
|
+
raise_sema_error("#{name} expects 1 or 2 arguments, got #{arguments.length}") unless arguments.length == 1 || arguments.length == 2
|
|
841
|
+
|
|
842
|
+
condition_type = infer_expression(arguments.fetch(0).value, scopes:, expected_type: @ctx.types.fetch("bool"))
|
|
843
|
+
ensure_assignable!(condition_type, @ctx.types.fetch("bool"), "#{name} condition must be bool, got #{condition_type}", expression: arguments.fetch(0).value)
|
|
844
|
+
check_assert_message(name, arguments[1]&.value, scopes:) if arguments.length == 2
|
|
845
|
+
|
|
846
|
+
@ctx.types.fetch("void")
|
|
847
|
+
end
|
|
848
|
+
def check_expect_eq_call(kind, arguments, scopes:)
|
|
849
|
+
name = kind.to_s
|
|
850
|
+
raise_sema_error("#{name} does not support named arguments") if arguments.any?(&:name)
|
|
851
|
+
raise_sema_error("#{name} expects 2 or 3 arguments, got #{arguments.length}") unless arguments.length == 2 || arguments.length == 3
|
|
852
|
+
|
|
853
|
+
left = arguments.fetch(0).value
|
|
854
|
+
right = arguments.fetch(1).value
|
|
855
|
+
operator = kind == :expect_ne ? "!=" : "=="
|
|
856
|
+
comparison = AST::BinaryOp.new(operator:, left:, right:, line: nil, column: nil)
|
|
857
|
+
infer_binary(comparison, scopes:, expected_type: @ctx.types.fetch("bool"))
|
|
858
|
+
check_assert_message(name, arguments[2]&.value, scopes:) if arguments.length == 3
|
|
859
|
+
|
|
860
|
+
@ctx.types.fetch("void")
|
|
861
|
+
end
|
|
862
|
+
def check_assert_message(name, message, scopes:)
|
|
863
|
+
return unless message
|
|
864
|
+
|
|
865
|
+
message_type = infer_expression(message, scopes:, expected_type: @ctx.types.fetch("str"))
|
|
866
|
+
raise_sema_error("#{name} message must be str or cstr, got #{message_type}") unless string_like_type?(message_type)
|
|
867
|
+
end
|
|
837
868
|
def check_get_call(arguments, scopes:)
|
|
838
869
|
raise_sema_error("get does not support named arguments") if arguments.any?(&:name)
|
|
839
870
|
raise_sema_error("get expects 2 arguments, got #{arguments.length}") unless arguments.length == 2
|
|
@@ -1043,6 +1043,10 @@ module MilkTea
|
|
|
1043
1043
|
check_order_call(callable, expression.arguments, scopes:)
|
|
1044
1044
|
when :fatal
|
|
1045
1045
|
check_fatal_call(expression.arguments, scopes:)
|
|
1046
|
+
when :assert, :expect
|
|
1047
|
+
check_assert_call(callable_kind, expression.arguments, scopes:)
|
|
1048
|
+
when :expect_eq, :expect_ne
|
|
1049
|
+
check_expect_eq_call(callable_kind, expression.arguments, scopes:)
|
|
1046
1050
|
when :ref_of
|
|
1047
1051
|
check_ref_of_call(expression.arguments, scopes:)
|
|
1048
1052
|
when :const_ptr_of
|
|
@@ -1318,6 +1322,10 @@ module MilkTea
|
|
|
1318
1322
|
|
|
1319
1323
|
return [:function, @ctx.top_level_functions.fetch(callee.name), nil] if @ctx.top_level_functions.key?(callee.name)
|
|
1320
1324
|
return [:fatal, nil, nil] if callee.name == "fatal"
|
|
1325
|
+
return [:assert, nil, nil] if callee.name == "assert"
|
|
1326
|
+
return [:expect, nil, nil] if callee.name == "expect"
|
|
1327
|
+
return [:expect_eq, nil, nil] if callee.name == "expect_eq"
|
|
1328
|
+
return [:expect_ne, nil, nil] if callee.name == "expect_ne"
|
|
1321
1329
|
return [:ref_of, nil, nil] if callee.name == "ref_of"
|
|
1322
1330
|
return [:const_ptr_of, nil, nil] if callee.name == "const_ptr_of"
|
|
1323
1331
|
return [:read, nil, nil] if callee.name == "read"
|
|
@@ -399,21 +399,14 @@ module MilkTea
|
|
|
399
399
|
return 1
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
-
testing_import = ast.imports.find { |import| import.path.parts == %w[std testing] }
|
|
403
|
-
unless testing_import
|
|
404
|
-
@err.puts("a test file must import std.testing: #{path}")
|
|
405
|
-
return 1
|
|
406
|
-
end
|
|
407
|
-
testing_alias = testing_import.alias_name || testing_import.path.parts.last
|
|
408
|
-
|
|
409
402
|
death_tests, normal_tests = tests.partition { |test| expect_fatal_attribute?(test) }
|
|
410
403
|
|
|
411
404
|
exit_code = 0
|
|
412
405
|
|
|
413
406
|
unless normal_tests.empty?
|
|
414
407
|
runner_source = source.dup
|
|
415
|
-
runner_source << "\n\n" << test_runner_main(
|
|
416
|
-
exit_code =
|
|
408
|
+
runner_source << "\n\n" << test_runner_main(normal_tests.map(&:name))
|
|
409
|
+
exit_code = run_normal_tests(path, runner_source, normal_tests.map(&:name), options:, locked:)
|
|
417
410
|
end
|
|
418
411
|
|
|
419
412
|
death_tests.each do |death_test|
|
|
@@ -459,30 +452,53 @@ module MilkTea
|
|
|
459
452
|
def death_test_runner_main(test_name)
|
|
460
453
|
[
|
|
461
454
|
"function main() -> int:",
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
" return 0",
|
|
465
|
-
" Result.failure:",
|
|
466
|
-
" return 0",
|
|
455
|
+
" #{test_name}()",
|
|
456
|
+
" return 0",
|
|
467
457
|
].join("\n") + "\n"
|
|
468
458
|
end
|
|
469
459
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
# The runner binary runs exactly one test per invocation, selected by
|
|
461
|
+
# `argv[1]`. This gives per-test isolation: an aborting assertion in one
|
|
462
|
+
# test cannot suppress the results of its siblings.
|
|
463
|
+
def test_runner_main(test_names)
|
|
464
|
+
lines = ["function main(args: span[str]) -> int:"]
|
|
465
|
+
lines << " let which = if args.len > 1: args[1] else: \"\""
|
|
466
|
+
lines << " match which:"
|
|
473
467
|
test_names.each do |name|
|
|
474
|
-
lines << "
|
|
468
|
+
lines << " \"#{name}\":"
|
|
469
|
+
lines << " #{name}()"
|
|
475
470
|
end
|
|
476
|
-
lines << "
|
|
471
|
+
lines << " _:"
|
|
472
|
+
lines << " return 2"
|
|
473
|
+
lines << " return 0"
|
|
477
474
|
lines.join("\n") + "\n"
|
|
478
475
|
end
|
|
479
476
|
|
|
480
|
-
def
|
|
477
|
+
def run_normal_tests(source_path, runner_source, test_names, options:, locked:)
|
|
481
478
|
with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
482
|
-
|
|
479
|
+
exit_code = 0
|
|
480
|
+
test_names.each do |name|
|
|
481
|
+
output, status, timed_out = spawn_sandboxed(binary_path, [name])
|
|
482
|
+
if timed_out
|
|
483
|
+
@out.puts("FAIL - #{name}: timed out")
|
|
484
|
+
exit_code = 1
|
|
485
|
+
elsif status&.exitstatus == 0
|
|
486
|
+
@out.puts("ok - #{name}")
|
|
487
|
+
else
|
|
488
|
+
@out.puts("FAIL - #{name}: #{first_fail_message(output)}")
|
|
489
|
+
exit_code = 1
|
|
490
|
+
end
|
|
491
|
+
@out.flush if @out.respond_to?(:flush)
|
|
492
|
+
end
|
|
493
|
+
exit_code
|
|
483
494
|
end
|
|
484
495
|
end
|
|
485
496
|
|
|
497
|
+
def first_fail_message(output)
|
|
498
|
+
line = output.to_s.lines.map(&:chomp).find { |entry| !entry.strip.empty? }
|
|
499
|
+
line.nil? || line.strip.empty? ? "test aborted" : line.strip
|
|
500
|
+
end
|
|
501
|
+
|
|
486
502
|
def with_synthesized_binary(source_path, runner_source, options:, locked:)
|
|
487
503
|
directory = File.dirname(File.expand_path(source_path))
|
|
488
504
|
runner_path = File.join(directory, "__mt_test_runner_#{Process.pid}.mt")
|
|
@@ -508,30 +524,13 @@ module MilkTea
|
|
|
508
524
|
end
|
|
509
525
|
end
|
|
510
526
|
|
|
511
|
-
def
|
|
512
|
-
output, status, timed_out = spawn_sandboxed(binary_path)
|
|
513
|
-
@out.write(output)
|
|
514
|
-
@out.flush if @out.respond_to?(:flush)
|
|
515
|
-
|
|
516
|
-
if timed_out
|
|
517
|
-
@err.puts("test run timed out after #{@test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS}s")
|
|
518
|
-
return 1
|
|
519
|
-
end
|
|
520
|
-
if status&.signaled?
|
|
521
|
-
@err.puts("test run crashed (signal #{status.termsig})")
|
|
522
|
-
return 1
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
status&.exitstatus || 1
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
def spawn_sandboxed(binary_path)
|
|
527
|
+
def spawn_sandboxed(binary_path, args = [])
|
|
529
528
|
timeout_seconds = @test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS
|
|
530
529
|
memory_bytes = @test_memory_bytes || TEST_RUN_MEMORY_LIMIT_BYTES
|
|
531
530
|
reader, writer = IO.pipe
|
|
532
531
|
spawn_options = { out: writer, err: writer, pgroup: true }
|
|
533
532
|
spawn_options[:rlimit_as] = memory_bytes unless @test_sanitize
|
|
534
|
-
pid = Process.spawn(binary_path, **spawn_options)
|
|
533
|
+
pid = Process.spawn(binary_path, *args, **spawn_options)
|
|
535
534
|
writer.close
|
|
536
535
|
|
|
537
536
|
status = nil
|
data/lib/milk_tea/tooling/cli.rb
CHANGED
|
@@ -597,10 +597,11 @@ module MilkTea
|
|
|
597
597
|
root), recursively discovers every .mt file that contains @[test] functions,
|
|
598
598
|
runs each as its own test binary, and prints an aggregate summary.
|
|
599
599
|
|
|
600
|
-
Functions annotated with @[test] must take no parameters and return
|
|
601
|
-
|
|
602
|
-
test
|
|
603
|
-
|
|
600
|
+
Functions annotated with @[test] must take no parameters and return void;
|
|
601
|
+
a failing test aborts via the `assert`/`expect`/`expect_eq`/`expect_ne`
|
|
602
|
+
intrinsics. `mtc test` synthesizes a runner that invokes each test in its
|
|
603
|
+
own process (for isolation) and reports the results. A test file must not
|
|
604
|
+
define `main`.
|
|
604
605
|
|
|
605
606
|
Each test binary runs under a wall-clock timeout and an address-space
|
|
606
607
|
memory cap so a hanging or runaway test cannot stall or exhaust the host.
|
|
@@ -105,14 +105,25 @@ module MilkTea
|
|
|
105
105
|
def terminating_expression?(expression)
|
|
106
106
|
case expression
|
|
107
107
|
when AST::Call
|
|
108
|
-
terminating_callee?(expression.callee) || static_assert_false?(expression)
|
|
108
|
+
terminating_callee?(expression.callee) || static_assert_false?(expression) || assertion_false?(expression)
|
|
109
109
|
when AST::Specialization
|
|
110
|
-
terminating_callee?(expression.callee) || static_assert_false?(expression)
|
|
110
|
+
terminating_callee?(expression.callee) || static_assert_false?(expression) || assertion_false?(expression)
|
|
111
111
|
else
|
|
112
112
|
false
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def assertion_false?(expression)
|
|
117
|
+
return false unless expression.is_a?(AST::Call)
|
|
118
|
+
|
|
119
|
+
callee = expression.callee
|
|
120
|
+
return false unless callee.is_a?(AST::Identifier)
|
|
121
|
+
return false unless %w[assert expect].include?(callee.name)
|
|
122
|
+
|
|
123
|
+
first_arg = expression.arguments.first
|
|
124
|
+
first_arg.is_a?(AST::BooleanLiteral) && first_arg.value == false
|
|
125
|
+
end
|
|
126
|
+
|
|
116
127
|
def static_assert_false?(expression)
|
|
117
128
|
return false unless expression.callee.is_a?(AST::Identifier)
|
|
118
129
|
return false unless expression.callee.name == "static_assert"
|
|
@@ -209,6 +209,7 @@ module MilkTea
|
|
|
209
209
|
check_prefer_try(statement.expression, statement.arms)
|
|
210
210
|
end
|
|
211
211
|
when AST::UnsafeStmt
|
|
212
|
+
check_redundant_unsafe(statement)
|
|
212
213
|
@unsafe_depth += 1
|
|
213
214
|
with_scope { visit_statement_list(statement.body) }
|
|
214
215
|
@unsafe_depth -= 1
|
|
@@ -314,6 +315,7 @@ module MilkTea
|
|
|
314
315
|
check_prefer_or_pattern(expression.arms, body_of: ->(arm) { arm.value })
|
|
315
316
|
end
|
|
316
317
|
when AST::UnsafeExpr
|
|
318
|
+
check_redundant_unsafe(expression)
|
|
317
319
|
@unsafe_depth += 1
|
|
318
320
|
visit_expression(expression.expression)
|
|
319
321
|
@unsafe_depth -= 1
|
|
@@ -362,6 +364,21 @@ module MilkTea
|
|
|
362
364
|
nil
|
|
363
365
|
end
|
|
364
366
|
end
|
|
367
|
+
def check_redundant_unsafe(node)
|
|
368
|
+
return unless @sema_facts
|
|
369
|
+
return if @sema_facts.required_unsafe_lines.include?(node.line)
|
|
370
|
+
|
|
371
|
+
@warnings << Warning.new(
|
|
372
|
+
path: @path,
|
|
373
|
+
line: node.line,
|
|
374
|
+
column: node.column,
|
|
375
|
+
length: "unsafe".length,
|
|
376
|
+
code: "redundant-unsafe",
|
|
377
|
+
message: "unsafe block contains no unsafe operations and can be removed",
|
|
378
|
+
severity: :hint,
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
|
|
365
382
|
def visit_type_argument(argument)
|
|
366
383
|
visit_expression(argument.value) if argument.respond_to?(:value)
|
|
367
384
|
end
|
|
@@ -22,17 +22,17 @@ module MilkTea
|
|
|
22
22
|
borrow-and-mutate
|
|
23
23
|
constant-condition
|
|
24
24
|
dead-assignment
|
|
25
|
-
duplicate-if-condition
|
|
26
25
|
directional-ffi-arg
|
|
27
26
|
doc-tag
|
|
27
|
+
duplicate-if-condition
|
|
28
28
|
event-capacity
|
|
29
29
|
line-too-long
|
|
30
30
|
loop-single-iteration
|
|
31
31
|
missing-return
|
|
32
32
|
noop-compound-assignment
|
|
33
|
-
platform-api-drift
|
|
34
|
-
owning-release-leak
|
|
35
33
|
owning-release-double
|
|
34
|
+
owning-release-leak
|
|
35
|
+
platform-api-drift
|
|
36
36
|
prefer-conditional-expression
|
|
37
37
|
prefer-inline-if
|
|
38
38
|
prefer-inline-methods
|
|
@@ -51,6 +51,7 @@ module MilkTea
|
|
|
51
51
|
redundant-null-check
|
|
52
52
|
redundant-return
|
|
53
53
|
redundant-type-annotation
|
|
54
|
+
redundant-unsafe
|
|
54
55
|
reserved-primitive-name
|
|
55
56
|
self-assignment
|
|
56
57
|
self-comparison
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mt-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Long (Teefan) Tran
|
|
@@ -150,6 +150,7 @@ files:
|
|
|
150
150
|
- docs/language-design.md
|
|
151
151
|
- docs/language-manual.md
|
|
152
152
|
- docs/lsp-performance.md
|
|
153
|
+
- docs/testing.md
|
|
153
154
|
- lib/milk_tea.rb
|
|
154
155
|
- lib/milk_tea/base.rb
|
|
155
156
|
- lib/milk_tea/bindings.rb
|
|
@@ -233,6 +234,7 @@ files:
|
|
|
233
234
|
- lib/milk_tea/core/lexer/trivia.rb
|
|
234
235
|
- lib/milk_tea/core/lowering.rb
|
|
235
236
|
- lib/milk_tea/core/lowering/artifacts.rb
|
|
237
|
+
- lib/milk_tea/core/lowering/assertions.rb
|
|
236
238
|
- lib/milk_tea/core/lowering/async/analysis.rb
|
|
237
239
|
- lib/milk_tea/core/lowering/async/async_lowering.rb
|
|
238
240
|
- lib/milk_tea/core/lowering/async/frame_builder.rb
|
|
@@ -609,7 +611,6 @@ files:
|
|
|
609
611
|
- std/sync.mt
|
|
610
612
|
- std/tar.mt
|
|
611
613
|
- std/terminal.mt
|
|
612
|
-
- std/testing.mt
|
|
613
614
|
- std/thread.mt
|
|
614
615
|
- std/time.mt
|
|
615
616
|
- std/timer.mt
|
|
@@ -629,7 +630,7 @@ metadata:
|
|
|
629
630
|
homepage_uri: https://teefan.github.io/mt-lang/
|
|
630
631
|
source_code_uri: https://github.com/teefan/mt-lang
|
|
631
632
|
post_install_message: |
|
|
632
|
-
Milk Tea 0.4.
|
|
633
|
+
Milk Tea 0.4.22 installed!
|
|
633
634
|
|
|
634
635
|
System requirements:
|
|
635
636
|
- A C compiler (gcc or clang) must be available on PATH
|
data/std/testing.mt
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
# Standard library: testing core (T0 prototype)
|
|
2
|
-
#
|
|
3
|
-
# Minimal, in-language unit-testing surface — see docs/testing.md (§5, T0).
|
|
4
|
-
# A test function returns `Check` and propagates the first failure with `?`:
|
|
5
|
-
#
|
|
6
|
-
# import std.testing as t
|
|
7
|
-
#
|
|
8
|
-
# function test_math() -> t.Check:
|
|
9
|
-
# t.expect_equal_int(2 + 2, 4)?
|
|
10
|
-
# return t.ok()
|
|
11
|
-
#
|
|
12
|
-
# A hand-written runner (see docs/testing.md §6 for future compiler-driven
|
|
13
|
-
# discovery) calls `record` per test and `summarize` at the end:
|
|
14
|
-
#
|
|
15
|
-
# function main() -> int:
|
|
16
|
-
# var stats = t.Stats.create()
|
|
17
|
-
# stats = t.record(stats, "math", test_math())
|
|
18
|
-
# return t.summarize(stats)
|
|
19
|
-
|
|
20
|
-
import std.string as string
|
|
21
|
-
import std.fmt as fmt
|
|
22
|
-
import std.stdio as stdio
|
|
23
|
-
import std.str
|
|
24
|
-
import std.hash
|
|
25
|
-
|
|
26
|
-
# A test outcome: success carries no meaningful value; failure carries a
|
|
27
|
-
# `Failure`. `Result` is used (not a bespoke variant) so `?` propagation works.
|
|
28
|
-
|
|
29
|
-
# A failed (or skipped) expectation. `message` is owned and must be released by
|
|
30
|
-
# whoever consumes it (the runner does this in `record`). `is_skip` distinguishes
|
|
31
|
-
# a skip from a real assertion failure.
|
|
32
|
-
public struct Failure:
|
|
33
|
-
message: string.String
|
|
34
|
-
is_skip: bool
|
|
35
|
-
|
|
36
|
-
public type Check = Result[bool, Failure]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Tally of outcomes for a run. Value type; `record` returns an updated copy.
|
|
40
|
-
public struct Stats:
|
|
41
|
-
passed: int
|
|
42
|
-
failed: int
|
|
43
|
-
skipped: int
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
extending Stats:
|
|
47
|
-
public static function create() -> Stats:
|
|
48
|
-
return Stats(passed = 0, failed = 0, skipped = 0)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# ── Outcome constructors ──────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
public function ok() -> Check:
|
|
54
|
-
return Result[bool, Failure].success(value = true)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
public function fail(message: str) -> Check:
|
|
58
|
-
return Result[bool, Failure].failure(error = Failure(message = string.String.from_str(message), is_skip = false))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
public function skip(reason: str) -> Check:
|
|
62
|
-
return Result[bool, Failure].failure(error = Failure(message = string.String.from_str(reason), is_skip = true))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# ── Expectations ──────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
public function expect(condition: bool, message: str) -> Check:
|
|
68
|
-
if condition:
|
|
69
|
-
return ok()
|
|
70
|
-
|
|
71
|
-
return fail(message)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
public function expect_true(condition: bool) -> Check:
|
|
75
|
-
return expect(condition, "expected true")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
public function expect_false(condition: bool) -> Check:
|
|
79
|
-
return expect(not condition, "expected false")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
public function expect_equal_int(actual: int, expected: int) -> Check:
|
|
83
|
-
if actual == expected:
|
|
84
|
-
return ok()
|
|
85
|
-
|
|
86
|
-
var message = string.String.create()
|
|
87
|
-
message.append("expected ")
|
|
88
|
-
fmt.append_int(ref_of(message), expected)
|
|
89
|
-
message.append(", got ")
|
|
90
|
-
fmt.append_int(ref_of(message), actual)
|
|
91
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
public function expect_equal_str(actual: str, expected: str) -> Check:
|
|
95
|
-
var actual_string = string.String.from_str(actual)
|
|
96
|
-
var expected_string = string.String.from_str(expected)
|
|
97
|
-
let same = actual_string.equal(expected_string)
|
|
98
|
-
actual_string.release()
|
|
99
|
-
expected_string.release()
|
|
100
|
-
if same:
|
|
101
|
-
return ok()
|
|
102
|
-
|
|
103
|
-
var message = string.String.create()
|
|
104
|
-
message.append("expected [")
|
|
105
|
-
message.append(expected)
|
|
106
|
-
message.append("], got [")
|
|
107
|
-
message.append(actual)
|
|
108
|
-
message.append("]")
|
|
109
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
public function expect_equal_bool(actual: bool, expected: bool) -> Check:
|
|
113
|
-
if actual == expected:
|
|
114
|
-
return ok()
|
|
115
|
-
|
|
116
|
-
var message = string.String.create()
|
|
117
|
-
message.append("expected ")
|
|
118
|
-
fmt.append_bool(ref_of(message), expected)
|
|
119
|
-
message.append(", got ")
|
|
120
|
-
fmt.append_bool(ref_of(message), actual)
|
|
121
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
public function expect_not_equal_int(actual: int, expected: int) -> Check:
|
|
125
|
-
if actual != expected:
|
|
126
|
-
return ok()
|
|
127
|
-
|
|
128
|
-
var message = string.String.create()
|
|
129
|
-
message.append("expected not ")
|
|
130
|
-
fmt.append_int(ref_of(message), expected)
|
|
131
|
-
message.append(", got ")
|
|
132
|
-
fmt.append_int(ref_of(message), actual)
|
|
133
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
public function expect_not_equal_str(actual: str, expected: str) -> Check:
|
|
137
|
-
var actual_string = string.String.from_str(actual)
|
|
138
|
-
var expected_string = string.String.from_str(expected)
|
|
139
|
-
let same = actual_string.equal(expected_string)
|
|
140
|
-
actual_string.release()
|
|
141
|
-
expected_string.release()
|
|
142
|
-
if not same:
|
|
143
|
-
return ok()
|
|
144
|
-
|
|
145
|
-
var message = string.String.create()
|
|
146
|
-
message.append("expected not [")
|
|
147
|
-
message.append(expected)
|
|
148
|
-
message.append("], got [")
|
|
149
|
-
message.append(actual)
|
|
150
|
-
message.append("]")
|
|
151
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
public function expect_not_equal_bool(actual: bool, expected: bool) -> Check:
|
|
155
|
-
if actual != expected:
|
|
156
|
-
return ok()
|
|
157
|
-
|
|
158
|
-
var message = string.String.create()
|
|
159
|
-
message.append("expected not ")
|
|
160
|
-
fmt.append_bool(ref_of(message), expected)
|
|
161
|
-
message.append(", got ")
|
|
162
|
-
fmt.append_bool(ref_of(message), actual)
|
|
163
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# Generic equality over any type with a canonical `T.equal` hook: primitives
|
|
167
|
-
# (import std.hash), `str` (import std.str), and user structs that define `equal`
|
|
168
|
-
# (or delegate to std.hash.equal_struct). On failure the actual/expected values
|
|
169
|
-
# are rendered via `std.fmt.format_value`, so `T` must be a primitive or a struct
|
|
170
|
-
# whose fields are themselves `format_value`-renderable.
|
|
171
|
-
public function expect_equal[T](actual: T, expected: T) -> Check:
|
|
172
|
-
if equal[T](actual, expected):
|
|
173
|
-
return ok()
|
|
174
|
-
|
|
175
|
-
var message = string.String.create()
|
|
176
|
-
message.append("expected ")
|
|
177
|
-
fmt.format_value[T](ref_of(message), const_ptr_of(expected))
|
|
178
|
-
message.append(", got ")
|
|
179
|
-
fmt.format_value[T](ref_of(message), const_ptr_of(actual))
|
|
180
|
-
return Result[bool, Failure].failure(error = Failure(message = message, is_skip = false))
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
public function expect_some[T](option: Option[T]) -> Check:
|
|
184
|
-
if option.is_some():
|
|
185
|
-
return ok()
|
|
186
|
-
|
|
187
|
-
return fail("expected Option.some, got Option.none")
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
public function expect_none[T](option: Option[T]) -> Check:
|
|
191
|
-
if option.is_none():
|
|
192
|
-
return ok()
|
|
193
|
-
|
|
194
|
-
return fail("expected Option.none, got Option.some")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
public function expect_null[T](pointer: const_ptr[T]?) -> Check:
|
|
198
|
-
if pointer == null:
|
|
199
|
-
return ok()
|
|
200
|
-
|
|
201
|
-
return fail("expected null pointer, got non-null")
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
public function expect_not_null[T](pointer: const_ptr[T]?) -> Check:
|
|
205
|
-
if pointer != null:
|
|
206
|
-
return ok()
|
|
207
|
-
|
|
208
|
-
return fail("expected non-null pointer, got null")
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
public function expect_error[T, E](result: Result[T, E]) -> Check:
|
|
212
|
-
if result.is_failure():
|
|
213
|
-
return ok()
|
|
214
|
-
|
|
215
|
-
return fail("expected Result.failure, got Result.success")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# ── Runner (hand-written; compiler discovery is a later phase) ─────────────
|
|
219
|
-
|
|
220
|
-
public function record(stats: Stats, name: str, outcome: Check) -> Stats:
|
|
221
|
-
match outcome:
|
|
222
|
-
Result.success:
|
|
223
|
-
var line = string.String.create()
|
|
224
|
-
line.append("ok - ")
|
|
225
|
-
line.append(name)
|
|
226
|
-
stdio.print_line(line.as_str())
|
|
227
|
-
line.release()
|
|
228
|
-
return Stats(passed = stats.passed + 1, failed = stats.failed, skipped = stats.skipped)
|
|
229
|
-
Result.failure as payload:
|
|
230
|
-
let failure = payload.error
|
|
231
|
-
var line = string.String.create()
|
|
232
|
-
if failure.is_skip:
|
|
233
|
-
line.append("skip - ")
|
|
234
|
-
else:
|
|
235
|
-
line.append("FAIL - ")
|
|
236
|
-
|
|
237
|
-
line.append(name)
|
|
238
|
-
line.append(": ")
|
|
239
|
-
line.append(failure.message.as_str())
|
|
240
|
-
stdio.print_line(line.as_str())
|
|
241
|
-
line.release()
|
|
242
|
-
|
|
243
|
-
var owned_message = failure.message
|
|
244
|
-
owned_message.release()
|
|
245
|
-
|
|
246
|
-
if failure.is_skip:
|
|
247
|
-
return Stats(passed = stats.passed, failed = stats.failed, skipped = stats.skipped + 1)
|
|
248
|
-
|
|
249
|
-
return Stats(passed = stats.passed, failed = stats.failed + 1, skipped = stats.skipped)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
public function summarize(stats: Stats) -> int:
|
|
253
|
-
var line = string.String.create()
|
|
254
|
-
line.append("passed=")
|
|
255
|
-
fmt.append_int(ref_of(line), stats.passed)
|
|
256
|
-
line.append(" failed=")
|
|
257
|
-
fmt.append_int(ref_of(line), stats.failed)
|
|
258
|
-
line.append(" skipped=")
|
|
259
|
-
fmt.append_int(ref_of(line), stats.skipped)
|
|
260
|
-
stdio.print_line(line.as_str())
|
|
261
|
-
line.release()
|
|
262
|
-
|
|
263
|
-
if stats.failed > 0:
|
|
264
|
-
return 1
|
|
265
|
-
|
|
266
|
-
return 0
|