5d: the review's image swap, teardown trap and fixture-name assertions
This commit is contained in:
@@ -200,9 +200,12 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
|
|||||||
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
|
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
|
||||||
over HTTP and converts the whole corpus, which is §6's browser half and the only SpiderMonkey
|
over HTTP and converts the whole corpus, which is §6's browser half and the only SpiderMonkey
|
||||||
there is — the gate's other three engines are two V8s and a JavaScriptCore that is not Safari's.
|
there is — the gate's other three engines are two V8s and a JavaScriptCore that is not Safari's.
|
||||||
geckodriver is what carries a verdict back out, the driver and the page's server sharing one
|
A WebDriver session is what carries a verdict back out, the driver and the page's server sharing
|
||||||
network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such
|
one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such
|
||||||
channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser.
|
channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser.
|
||||||
|
The leg re-checks the conversions and nothing else — each fixture's emitted markdown, its parsed
|
||||||
|
document, its error code — leaving the corpus's pairing, uniqueness, source positions and
|
||||||
|
byte-level equality to the Node suite that owns them.
|
||||||
|
|
||||||
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
||||||
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
try {
|
try {
|
||||||
const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js')
|
const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js')
|
||||||
window.convertCorpus = (corpus) => ({
|
window.convertCorpus = (corpus) => ({
|
||||||
errors: corpus.errors.map(({ markdown }) => markdownToAdf(markdown)),
|
errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
|
||||||
normalization: corpus.normalization.map(({ markdown }) => markdownToAdf(markdown)),
|
normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
|
||||||
roundTrip: corpus.roundTrip.map(({ json, markdown }) => {
|
roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => {
|
||||||
const adf = JSON.parse(json)
|
const adf = JSON.parse(json)
|
||||||
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), parsed: markdownToAdf(markdown) }
|
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) }
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
|
|||||||
+24
-15
@@ -9,7 +9,8 @@ const repoRoot = join(import.meta.dirname, '..')
|
|||||||
const corpusRoot = join(repoRoot, 'corpus')
|
const corpusRoot = join(repoRoot, 'corpus')
|
||||||
const corpusEntries = readdirSync(corpusRoot, { encoding: 'utf8', recursive: true })
|
const corpusEntries = readdirSync(corpusRoot, { encoding: 'utf8', recursive: true })
|
||||||
|
|
||||||
function checking(name, assertions) {
|
function checking(name, result, assertions) {
|
||||||
|
assert.equal(result?.name, name, `the browser answered ${JSON.stringify(result?.name)} where ${name} was sent`)
|
||||||
try {
|
try {
|
||||||
assertions()
|
assertions()
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -57,7 +58,7 @@ async function whenDriverAnswers() {
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
const status = await command('GET', '/status').catch(() => undefined)
|
const status = await command('GET', '/status').catch(() => undefined)
|
||||||
if (status?.ready === true) return
|
if (status?.ready === true) return
|
||||||
if (Date.now() > deadline) throw new Error(`no geckodriver answered ${driver}/status within 60s`)
|
if (Date.now() > deadline) throw new Error(`no WebDriver answered ${driver}/status within 60s`)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +68,14 @@ const corpus = {
|
|||||||
normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
|
normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
|
||||||
roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })),
|
roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })),
|
||||||
}
|
}
|
||||||
|
assert.deepEqual(
|
||||||
|
readdirSync(corpusRoot, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort(),
|
||||||
|
['errors', 'normalization', 'round-trip'],
|
||||||
|
'a corpus kind the browser leg does not convert',
|
||||||
|
)
|
||||||
for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`)
|
for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`)
|
||||||
|
|
||||||
const server = createServer((request, response) => {
|
const server = createServer((request, response) => {
|
||||||
@@ -88,9 +97,9 @@ const results = await command('POST', `/session/${session.sessionId}/execute/syn
|
|||||||
})
|
})
|
||||||
server.close()
|
server.close()
|
||||||
|
|
||||||
for (const [index, result] of results.roundTrip.entries()) {
|
for (const [index, { json, markdown, name }] of corpus.roundTrip.entries()) {
|
||||||
const { json, markdown, name } = corpus.roundTrip[index]
|
const result = results.roundTrip[index]
|
||||||
checking(name, () => {
|
checking(name, result, () => {
|
||||||
assert.ok(result.isDocument, `${name}.json is no ADF document`)
|
assert.ok(result.isDocument, `${name}.json is no ADF document`)
|
||||||
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
|
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
|
||||||
assert.equal(result.emitted.value, markdown)
|
assert.equal(result.emitted.value, markdown)
|
||||||
@@ -99,19 +108,19 @@ for (const [index, result] of results.roundTrip.entries()) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, result] of results.normalization.entries()) {
|
for (const [index, { name }] of corpus.normalization.entries()) {
|
||||||
const { name } = corpus.normalization[index]
|
const result = results.normalization[index]
|
||||||
checking(name, () => {
|
checking(name, result, () => {
|
||||||
assert.ok(result.ok, `it did not parse — ${refusal(result)}`)
|
assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`)
|
||||||
assert.deepEqual(result.value, JSON.parse(fixture(name, '.json')))
|
assert.deepEqual(result.parsed.value, JSON.parse(fixture(name, '.json')))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, result] of results.errors.entries()) {
|
for (const [index, { name }] of corpus.errors.entries()) {
|
||||||
const { name } = corpus.errors[index]
|
const result = results.errors[index]
|
||||||
checking(name, () => {
|
checking(name, result, () => {
|
||||||
assert.ok(!result.ok, `it was not refused, it built ${JSON.stringify(result.value)}`)
|
assert.ok(!result.parsed.ok, `it was not refused, it built ${JSON.stringify(result.parsed.value)}`)
|
||||||
assert.equal(result.error.code, fixture(name, '.error').trimEnd())
|
assert.equal(result.parsed.error.code, fixture(name, '.error').trimEnd())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,20 +1,21 @@
|
|||||||
bun_image=oven/bun:1.4.0-alpine
|
bun_image=oven/bun:1.4.0-alpine
|
||||||
deno_image=denoland/deno:2.9.6
|
deno_image=denoland/deno:2.9.6
|
||||||
firefox_image=instrumentisto/geckodriver:149.0.2
|
firefox_image=selenium/standalone-firefox:153.0.4
|
||||||
floor_image=node:18.20.8-alpine3.21
|
floor_image=node:18.20.8-alpine3.21
|
||||||
node_image=node:24.19.0-alpine3.24
|
node_image=node:24.19.0-alpine3.24
|
||||||
|
|
||||||
in_image() {
|
in_image() {
|
||||||
local image=$1 entrypoint=$2
|
local image=$1 entrypoint=$2
|
||||||
shift 2
|
shift 2
|
||||||
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp ${network:+--network "$network"} -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@"
|
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp ${in_image_network:+--network "$in_image_network"} -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
with_firefox() {
|
with_firefox() {
|
||||||
local container network status=0
|
local container in_image_network status=0
|
||||||
container=$(docker run -d --rm "$firefox_image")
|
container=$(docker run -d --rm "$firefox_image")
|
||||||
network="container:$container"
|
# The id is baked in: the trap fires after this function's locals are gone.
|
||||||
|
trap "docker rm -f $container >/dev/null 2>&1" EXIT INT TERM
|
||||||
|
in_image_network="container:$container"
|
||||||
"$@" || status=$?
|
"$@" || status=$?
|
||||||
docker rm -f "$container" >/dev/null
|
|
||||||
return $status
|
return $status
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -11,15 +11,6 @@
|
|||||||
"matchStrings": ["denoland/deno:(?<currentValue>[0-9][^\\s\"']*)"],
|
"matchStrings": ["denoland/deno:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||||
"versioningTemplate": "docker"
|
"versioningTemplate": "docker"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"customType": "regex",
|
|
||||||
"datasourceTemplate": "docker",
|
|
||||||
"depNameTemplate": "instrumentisto/geckodriver",
|
|
||||||
"description": "Pin the Firefox image the gate's browser leg runs",
|
|
||||||
"managerFilePatterns": ["docker-runner.sh"],
|
|
||||||
"matchStrings": ["instrumentisto/geckodriver:(?<currentValue>[0-9][^\\s\"']*)"],
|
|
||||||
"versioningTemplate": "docker"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"datasourceTemplate": "docker",
|
"datasourceTemplate": "docker",
|
||||||
@@ -56,6 +47,15 @@
|
|||||||
"managerFilePatterns": [".gitea/workflows/renovate.yml"],
|
"managerFilePatterns": [".gitea/workflows/renovate.yml"],
|
||||||
"matchStrings": ["renovate/renovate:(?<currentValue>[0-9][^\\s\"']*)"],
|
"matchStrings": ["renovate/renovate:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||||
"versioningTemplate": "docker"
|
"versioningTemplate": "docker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"datasourceTemplate": "docker",
|
||||||
|
"depNameTemplate": "selenium/standalone-firefox",
|
||||||
|
"description": "Pin the Firefox image the gate's browser leg runs",
|
||||||
|
"managerFilePatterns": ["docker-runner.sh"],
|
||||||
|
"matchStrings": ["selenium/standalone-firefox:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||||
|
"versioningTemplate": "docker"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extends": ["config:recommended"],
|
"extends": ["config:recommended"],
|
||||||
|
|||||||
+12
-7
@@ -519,11 +519,16 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
JavaScriptCore that is not Safari's. The mechanism is the decision this item opens with: a
|
JavaScriptCore that is not Safari's. The mechanism is the decision this item opens with: a
|
||||||
browser leg wants an image, a driver and a way to carry a verdict back out, none of which
|
browser leg wants an image, a driver and a way to carry a verdict back out, none of which
|
||||||
the gate's plain `docker run` per engine has. The answer is `with_firefox`, which runs the
|
the gate's plain `docker run` per engine has. The answer is `with_firefox`, which runs the
|
||||||
geckodriver image beside the node one in a shared network namespace, so the page's server and
|
Firefox image beside the node one in a shared network namespace, so the page's server and
|
||||||
the driver are each other's `127.0.0.1` and no user-defined network, container name or
|
the driver are each other's `127.0.0.1` and no user-defined network, container name or
|
||||||
geckodriver `--allow-hosts` entry is wanted. `browser-tests/run.js` serves the repo, drives
|
geckodriver `--allow-hosts` entry is wanted; its `EXIT INT TERM` trap bakes in the container
|
||||||
one `execute/sync` and asserts the results against the corpus with the Node-side
|
id, since the `local` holding it is gone by the time the trap fires. `browser-tests/run.js`
|
||||||
`assert.deepEqual` the corpus runner uses, so the browser page holds no second copy of the
|
serves the repo, drives one `execute/sync` and asserts the results against the corpus with the
|
||||||
comparison. The whole corpus fits: 118 fixtures in 2.9s warm, 14s cold, against a 120s
|
Node-side `assert.deepEqual` the corpus runner uses, so the browser page holds no second copy
|
||||||
script timeout — no slice was worth choosing. A `try` around the dynamic import is what turns
|
of the comparison. The whole corpus fits: 118 fixtures in 8s warm against a 120s script
|
||||||
a broken build into SpiderMonkey's own message rather than an undefined global.
|
timeout — no slice was worth choosing. A `try` around the dynamic import is what turns a
|
||||||
|
broken build into SpiderMonkey's own message rather than an undefined global.
|
||||||
|
**Settled** (the maintainer, 2026-09-04): `selenium/standalone-firefox` over the smaller
|
||||||
|
`instrumentisto/geckodriver`, currency over size — the leg's whole worth is a real
|
||||||
|
SpiderMonkey, which decays the moment the pin stops moving, and the smaller image was four
|
||||||
|
Firefox majors behind with a publisher that may go quiet while Renovate stays silent.
|
||||||
|
|||||||
Reference in New Issue
Block a user