Skip to content

Commit 934ff56

Browse files
authored
test: run the conformance corpus from Tish, the third target (#12)
The corpus exists to make "one source, three targets" a fact rather than a claim, and it was only ever run by two of them: the JS build and the Rust crate. The Tish source itself — what both are generated FROM — was never checked against it. A change that altered the Tish semantics but survived both emits would have gone unnoticed by the very thing meant to catch it. test/conformance.tish imports ../src/index.tish and runs the same inputs against the same expected files. All 11 cases pass, so the three targets are now demonstrably in agreement rather than assumed to be. Two things it has to do differently, both noted where they happen: Run under `tish run` instead of the JS build, because reading the corpus needs `tish:fs` and the JS target has no filesystem at all. Compare semantically rather than textually. This runtime's JSON.stringify ignores its indent argument and emits compact output, so comparing against the pretty-printed expected file failed on whitespace for all 11 cases while the data matched exactly. Re-stringifying the parsed expectation puts both sides in one form; key order survives the round-trip, so real drift is still caught.
1 parent 5ccb400 commit 934ff56

4 files changed

Lines changed: 95 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ jobs:
2929
- name: Run tests with coverage
3030
run: npm run test:coverage
3131

32-
- name: Conformance corpus
32+
- name: Conformance corpus (JS)
3333
run: npm run test:conformance
3434

35+
# The third target. The JS build and the Rust crate were both checked against the corpus; the
36+
# Tish source itself was not, so "one source, three targets" had a leg missing.
37+
- name: Conformance corpus (Tish)
38+
run: npm run test:conformance:tish
39+
3540
- name: Examples
3641
run: npm run examples
3742

conformance/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ must produce the **same** parse:
55

66
| Implementation | Runner |
77
|----------------|--------|
8-
| JS (`dist/deck.js`) | `npm run test:conformance` |
9-
| Rust (`deckfile` crate, emitted from the same Tish source) | `cargo test` in the crate |
8+
| **Tish** (`src/index.tish` — the source itself) | `npm run test:conformance:tish` |
9+
| **JS** (`dist/deck.js`) | `npm run test:conformance` |
10+
| **Rust** (`deckfile` crate, emitted from the same source) | `cargo test` in the crate |
1011
| A restricted host (tish-gba) | its own test, against the `gba` profile |
1112

13+
The Tish runner uses `tish run`, not the JS build, because reading the corpus needs `tish:fs` and the
14+
JS target has no filesystem. It also compares **semantically**`JSON.stringify` ignores its indent
15+
argument on that runtime, so a text compare would fail on whitespace while the data matched.
16+
1217
Each case is `NNN-name.deck` plus `NNN-name.expected.json`, which holds the full observable parse:
1318

1419
```jsonc

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@
4343
"scripts": {
4444
"build": "tish build src/index.tish -o dist/deck.js --target js && node scripts/append-exports.mjs",
4545
"tishversion": "tish -V",
46-
"test": "npm run build && node test/coverage.mjs && node test/conformance.mjs && tish build --target js test/smoke.tish -o /tmp/spacedevin-deck-smoke.js && node /tmp/spacedevin-deck-smoke.js",
46+
"test": "npm run build && node test/coverage.mjs && node test/conformance.mjs && npm run test:conformance:tish && tish build --target js test/smoke.tish -o /tmp/spacedevin-deck-smoke.js && node /tmp/spacedevin-deck-smoke.js",
4747
"test:conformance": "npm run build && node test/conformance.mjs",
4848
"conformance:update": "npm run build && node test/conformance.mjs --update",
4949
"test:coverage": "npm run build && c8 --check-coverage --lines 100 --functions 100 --statements 100 --include 'dist/deck.js' node test/coverage.mjs",
5050
"examples": "tish build --target js examples/01-parse.tish -o /tmp/deck-ex01.js && node /tmp/deck-ex01.js && tish build --target js examples/02-host-boot.tish -o /tmp/deck-ex02.js && node /tmp/deck-ex02.js && tish build --target js examples/03-helpers.tish -o /tmp/deck-ex03.js && node /tmp/deck-ex03.js",
5151
"prepack": "npm run build",
5252
"prepublishOnly": "npm run build",
5353
"build:rust": "node scripts/build-rust.mjs",
54-
"test:rust": "npm run build:rust && cd crate && cargo test"
54+
"test:rust": "npm run build:rust && cd crate && cargo test",
55+
"test:conformance:tish": "tish run test/conformance.tish"
5556
},
5657
"c8": {
5758
"reporter": [

test/conformance.tish

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Conformance corpus runner — the TISH target.
2+
//
3+
// The corpus is what turns "one source, three targets" from a claim into a fact, and it was only
4+
// being run by the JS build (test/conformance.mjs) and the Rust crate (rust/conformance.rs). This is
5+
// the third leg: it imports ../src/index.tish, so it exercises the Tish source itself rather than
6+
// something compiled from it.
7+
//
8+
// Run with `tish run` rather than the JS build, because the JS target has no filesystem — reading
9+
// the corpus needs `tish:fs`.
10+
//
11+
// npm run test:conformance:tish
12+
import { readDir, readFile } from "tish:fs"
13+
import { parseProgram, parseTrackBody } from "../src/index.tish"
14+
15+
let failed = 0
16+
fn check(name, cond) {
17+
if (cond) {
18+
console.log("ok " + name)
19+
} else {
20+
console.log("FAIL " + name)
21+
failed = failed + 1
22+
}
23+
}
24+
25+
/// Same observable parse the other two runners snapshot: the program AST plus every track's and
26+
/// clip's parsed body.
27+
fn snapshotOf(src) {
28+
let program = parseProgram(src)
29+
let trackBodies = []
30+
let ti = 0
31+
while (ti < program.tracks.length) {
32+
let t = program.tracks[ti]
33+
let parsed = parseTrackBody(t.body)
34+
trackBodies.push({ id: t.id, rows: parsed.rows, errors: parsed.errors })
35+
ti = ti + 1
36+
}
37+
let clipBodies = []
38+
let ci = 0
39+
while (ci < program.clipBlocks.length) {
40+
let c = program.clipBlocks[ci]
41+
let parsed = parseTrackBody(c.body)
42+
clipBodies.push({ clipId: c.clipId, rows: parsed.rows, errors: parsed.errors })
43+
ci = ci + 1
44+
}
45+
return { program: program, trackBodies: trackBodies, clipBodies: clipBodies }
46+
}
47+
48+
let entries = readDir("conformance")
49+
let cases = []
50+
let ni = 0
51+
while (ni < entries.length) {
52+
let n = String(entries[ni])
53+
if (n.length > 5 && n.substring(n.length - 5) === ".deck") {
54+
cases.push(n.substring(0, n.length - 5))
55+
}
56+
ni = ni + 1
57+
}
58+
cases.sort()
59+
check("corpus present", cases.length >= 10)
60+
61+
let ki = 0
62+
while (ki < cases.length) {
63+
let name = cases[ki]
64+
// Compare SEMANTICALLY, not textually: this runtime's JSON.stringify ignores the indent argument
65+
// and emits compact output, so a text compare against the pretty-printed expected file would fail
66+
// on whitespace while the data matched exactly. Re-stringifying the parsed expectation puts both
67+
// sides in the same form; key order survives the round-trip, so this still catches real drift.
68+
let actual = JSON.stringify(snapshotOf(readFile("conformance/" + name + ".deck")))
69+
let expected = JSON.stringify(JSON.parse(readFile("conformance/" + name + ".expected.json")))
70+
check(name, actual === expected)
71+
ki = ki + 1
72+
}
73+
74+
if (failed > 0) {
75+
console.log(String(failed) + " FAILED")
76+
process.exit(1)
77+
}
78+
console.log("")
79+
console.log("CONFORMANCE_OK (tish) — " + String(cases.length) + " cases")

0 commit comments

Comments
 (0)