diff --git a/harness/src/runner.ts b/harness/src/runner.ts index ce7edfd..150440c 100644 --- a/harness/src/runner.ts +++ b/harness/src/runner.ts @@ -291,13 +291,15 @@ class UnsupportedDirective extends Error {} /** * Trap-message matching: official interpreters compare expected wast text by - * prefix/substring against the actual message. Our runtime's trap wording + * substring against the actual message. Our runtime's trap wording * (runtime/src/cabi, runtime/src/exec) was ported/written independently of * the suite's expected strings and is semantically correct but differently * worded in several spots (confirmed against `trapIf(...)` call sites) — - * these pairs are normalized here rather than left as false failures. A - * message pair not in this table falls back to plain substring matching, so - * new/incidental wording matches keep working without a table entry. + * these exact pairs are recognized here rather than left as false failures. + * A message pair not in this table falls back to plain substring matching, as + * ordinary WAST assertions require. Exact equality prevents an equivalence + * for one operation from accepting a diagnostic which merely contains it, + * notably a later refusal carrying the original trap as its poison cause. * * Rows also cover engine-worded *core*-wasm traps: `mapCoreException` in * runtime/src/exec/boundary.ts passes a `WebAssembly.RuntimeError`'s message @@ -307,15 +309,33 @@ class UnsupportedDirective extends Error {} * against the suite's expected (typically wasmtime-worded) text instead. */ const TRAP_MESSAGE_EQUIVALENTS: Array< - [expectedPrefix: string, actualSubstrings: string[]] + [exactExpected: string, exactActuals: string[]] > = [ - // resources/handle-table.wast: runtime/src/cabi/handles.ts Table.get/free. + // Official resources/handle-table.wast:201-213,261,293. Table.get emits + // one of these two exact diagnostics (runtime/src/cabi/handles.ts:33-38). + ["unknown handle index 5", ["table index out of range"]], + [ + "unknown handle index 1", + ["table index out of range", "table entry empty"], + ], + ["unknown handle index 0", ["table entry empty"]], + ["unknown handle index 4294967295", ["table index out of range"]], + // async/passing-resources.wast:176 reaches an allocated-then-empty slot 3. + [ + "unknown handle index 3", + ["table index out of range", "table entry empty"], + ], + // Pinned Wasmtime resources.wast:296,338,379,435,531,597,665 uses the + // exact generic category for both never-allocated and vacated entries. ["unknown handle index", ["table index out of range", "table entry empty"]], - // resources/handle-table.wast: runtime/src/cabi/handles.ts lift/lowerOwn - // /Borrow resource-type checks (4 call sites, identical message). + // Pinned Wasmtime resources.wast:459-481 passes literal slot 2 to the + // outer component's empty table; Table.get rejects it as out of range. + ["unknown handle index 2", ["table index out of range"]], + // Official resources/handle-table.wast:322,324 and the corresponding + // resource-type checks in runtime/src/cabi/handles.ts:239-263. [ - "handle index", - ["resource type mismatch"], // "... used with the wrong type, expected ..." + "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource", + ["resource type mismatch"], ], // async/builtin-trap-poisons-instance.wast:9 assert_trap "wasm trap: wasm // `unreachable` instruction executed" — core `unreachable` trap, raw engine @@ -326,9 +346,10 @@ const TRAP_MESSAGE_EQUIVALENTS: Array< [ "wasm trap: wasm `unreachable` instruction executed", [ - "unreachable", - "unreachable executed", - "Unreachable code should not be executed", + "guest trapped: unreachable", + "guest trapped: unreachable executed", + "guest trapped: Unreachable code should not be executed", + "guest trapped: Unreachable code should not be executed (evaluating 'fn(...args)')", ], ], // async/big-interleaving-test.wast:836 asserts the SHORT form, plain @@ -340,7 +361,10 @@ const TRAP_MESSAGE_EQUIVALENTS: Array< // empty at the playwright pin bump (polyengine#11). [ "unreachable", - ["Unreachable code should not be executed"], + [ + "guest trapped: Unreachable code should not be executed", + "guest trapped: Unreachable code should not be executed (evaluating 'fn(...args)')", + ], ], // Wasmtime's supplementary corpus uses the bare canonical core-trap name // (crates/environ/src/trap_encoding.rs:138), unlike the official corpus's @@ -352,6 +376,7 @@ const TRAP_MESSAGE_EQUIVALENTS: Array< "guest trapped: unreachable", "guest trapped: unreachable executed", "guest trapped: Unreachable code should not be executed", + "guest trapped: Unreachable code should not be executed (evaluating 'fn(...args)')", ], ], // definitions.py:2344-2355 gives inc-at-2^16 and dec-below-zero the same @@ -408,10 +433,8 @@ const TRAP_MESSAGE_EQUIVALENTS: Array< // assert_trap command. export function trapMatches(expected: string, actual: string): boolean { if (actual.includes(expected)) return true; - for (const [prefix, actuals] of TRAP_MESSAGE_EQUIVALENTS) { - if ( - expected.startsWith(prefix) && actuals.some((a) => actual.includes(a)) - ) { + for (const [exactExpected, exactActuals] of TRAP_MESSAGE_EQUIVALENTS) { + if (expected === exactExpected && exactActuals.includes(actual)) { return true; } } diff --git a/harness/tests/runner_unit_test.ts b/harness/tests/runner_unit_test.ts index c1c146e..6183ead 100644 --- a/harness/tests/runner_unit_test.ts +++ b/harness/tests/runner_unit_test.ts @@ -204,7 +204,7 @@ Deno.test("assert_uninstantiable rejects an unrelated trap cause", async () => { assertEq(result.results[0].status, "failed", "status"); }); -// TRAP_MESSAGE_EQUIVALENTS: the core `unreachable` trap row. The runtime +// Exact diagnostic equivalents: the core `unreachable` trap row. The runtime // (runtime/src/exec/boundary.ts mapCoreException) passes each JS engine's raw // trap text through untouched; this table is where the suite's // (wasmtime-worded) expected text is reconciled against each engine's own @@ -263,6 +263,66 @@ Deno.test("trapMatches: an unrelated engine trap message does not falsely match ); }); +Deno.test("trapMatches: Bun's exact core `unreachable` diagnostic matches every corpus expectation", () => { + const actual = + "guest trapped: Unreachable code should not be executed (evaluating 'fn(...args)')"; + for ( + const expected of [ + "wasm trap: wasm `unreachable` instruction executed", + "unreachable", + "wasm `unreachable` instruction executed", + ] + ) { + assertEq(trapMatches(expected, actual), true, expected); + assertEq( + trapMatches(expected, `${actual} additional suffix`), + false, + `${expected} rejects an unverified suffix`, + ); + } +}); + +Deno.test("trapMatches: ordinary WAST matching remains actual.includes(expected)", () => { + assertEq( + trapMatches("unreachable", "guest trapped: unreachable executed"), + true, + "ordinary substring", + ); +}); + +Deno.test("trapMatches: handle-table equivalents require the full corpus diagnostic", () => { + const exactPairs: Array<[string, string]> = [ + ["unknown handle index 5", "table index out of range"], + ["unknown handle index 1", "table index out of range"], + ["unknown handle index 1", "table entry empty"], + ["unknown handle index 0", "table entry empty"], + ["unknown handle index 4294967295", "table index out of range"], + ["unknown handle index 3", "table index out of range"], + ["unknown handle index 3", "table entry empty"], + ["unknown handle index", "table index out of range"], + ["unknown handle index", "table entry empty"], + ["unknown handle index 2", "table index out of range"], + [ + "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource", + "resource type mismatch", + ], + ]; + for (const [expected, actual] of exactPairs) { + assertEq(trapMatches(expected, actual), true, `${expected} / ${actual}`); + } + + const nonExactPairs: Array<[string, string]> = [ + ["unknown handle index extended", "table index out of range"], + ["prefix unknown handle index 5", "table index out of range"], + ["unknown handle index 5 suffix", "table index out of range"], + ["unknown handle index 5", "prefix table index out of range"], + ["unknown handle index 5", "table index out of range suffix"], + ]; + for (const [expected, actual] of nonExactPairs) { + assertEq(trapMatches(expected, actual), false, `${expected} / ${actual}`); + } +}); + Deno.test("trapMatches: verified diagnostic equivalents match only their named operations", () => { const equivalents: Array<[string, string]> = [ ["backpressure counter overflow", "backpressure counter underflow"], @@ -325,3 +385,33 @@ Deno.test("trapMatches: narrow diagnostic rows reject adjacent but different tra assertEq(trapMatches(expected, actual), false, `${expected} / ${actual}`); } }); + +Deno.test("trapMatches: an equivalent poison cause does not match a later entry refusal", () => { + assertEq( + trapMatches( + "backpressure counter overflow", + "component entry refused — instance poisoned by: Trap: backpressure counter underflow", + ), + false, + "poison cause substring", + ); +}); + +Deno.test("trapMatches: exact equivalents reject expected and actual affixes", () => { + const expected = "backpressure counter overflow"; + const actual = "backpressure counter underflow"; + for ( + const [changedExpected, changedActual] of [ + [`prefix ${expected}`, actual], + [`${expected} suffix`, actual], + [expected, `prefix ${actual}`], + [expected, `${actual} suffix`], + ] + ) { + assertEq( + trapMatches(changedExpected, changedActual), + false, + `${changedExpected} / ${changedActual}`, + ); + } +});