Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,44 @@ jobs:
env:
SHARD: ${{ matrix.shard }}
run: pnpm exec vitest --run --config oracle-gaps/vitest.config.mjs gap-report-oracle -t "$(node scripts/pr-shards.mjs pattern oracle-gaps-fixed-seed-windows "$SHARD")"
daemon-history-runs:
# Temporary diagnostic (the per-run log of the daemon history); remove before merge.
name: Daemon history run log (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
continue-on-error: true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Tools
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- run: pnpm --filter @tanstack/oracles run build
- name: Daemon history, with its run log
shell: bash
working-directory: packages/oracles
env:
GAP_RUN_LOG: ${{ runner.temp }}/runs.jsonl
run: |
start=$(date +%s)
pnpm exec vitest --run --testTimeout=2700000 --reporter=verbose --config oracle-gaps/vitest.config.mjs gap-report-oracle -t 'fixed seed 20261105: an edit sequence through the daemon$' || true
echo "wall $(( $(date +%s) - start )) s"
node -e '
const fs = require("fs")
const rows = fs.readFileSync(process.argv[1], "utf8").trim().split("\n").map((l) => JSON.parse(l))
const total = rows.reduce((s, r) => s + r.ms, 0)
console.log("runs logged: " + rows.length + ", total ms " + total)
console.log("killed by the driver timeout: " + rows.filter((r) => r.killedByTimeout).length)
for (const r of rows.filter((r) => r.ms > 20000)) console.log("long run: " + JSON.stringify(r))
const by = {}
for (const r of rows) { const k = r.via + " status " + r.status + " signal " + r.signal; by[k] = (by[k] ?? 0) + 1 }
console.log(JSON.stringify(by, null, 1))
' "$GAP_RUN_LOG"
windows-gap-cli:
name: Windows gap CLI on a fixture
runs-on: windows-latest
Expand Down
15 changes: 3 additions & 12 deletions packages/oracles/oracle-gaps/gap-report-model-oracle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4257,8 +4257,7 @@ function baselineOf(plan) {
// baseline too.
const own =
plan.tests.filter((t) => t.file === file).length +
(file === `a` && plan.consoleOutput ? 1 : 0) +
(file === `a` && plan.aborts ? 1 : 0)
(file === `a` && plan.consoleOutput ? 1 : 0)
const broken = failing.find((f) => f.file === file)
// The failing owner's suite holds two tests and a third of its name.
const extra = broken?.test === undefined ? 0 : 3
Expand Down Expand Up @@ -4892,16 +4891,8 @@ function oracleTestFile(plan, file) {
`})`,
`test('fixed seed 999', () => {})`,
)
// A test that ends the run's process by SIGABRT when the driver asks
// (`GAP_ABORT`), as a V8 out-of-memory abort does: in worker threads its
// process is the run's, and `process.abort()` is unavailable there. It
// runs and passes in every other run.
if (file === `a` && plan.aborts)
out.push(
`test('aborts the process when asked', () => {`,
` if (process.env.GAP_ABORT === '1') process.kill(process.pid, 'SIGABRT')`,
`})`,
)
// The abort of an "aborts" run comes from the CLI's own check process
// (ORACLE_GAPS_TEST_ABORT_AT), so no test of file a is needed for it.
// A file with no tests still loads, so the CLI can read what it owns.
if (
!plan.tests.some((t) => t.file === file) &&
Expand Down
56 changes: 46 additions & 10 deletions packages/oracles/oracle-gaps/gap-report-oracle.property.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@
*/
import { spawn, spawnSync } from 'node:child_process'
import {
appendFileSync,
cpSync,
existsSync,
mkdirSync,
Expand All @@ -491,7 +492,7 @@ import {
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, relative, resolve } from 'node:path'
import { basename, dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import fc from 'fast-check'
import { socketOf, stateDirOf } from './daemon.mjs'
Expand Down Expand Up @@ -653,18 +654,36 @@ const configArgs = (plan) => ({
* Writes the fixture's files and runs the CLI on it. A file of an earlier
* state that this state lacks is removed.
*/
// Per-run log (GAP_RUN_LOG, a file the driver appends to): each CLI run's
// start and end, its status or signal, and whether the driver's kill timeout
// ended it. Test-side only; the CLI never sees it.
const runLog = process.env.GAP_RUN_LOG
const logRun = (entry) => {
if (runLog !== undefined) appendFileSync(runLog, `${JSON.stringify(entry)}\n`)
}

function runCli(dir, files, how = {}) {
writeFixture(dir, files)
const before = new Set(filesUnder(dir))
const { args, out } = cliArgs(dir, files, how)
if (how.logBlocked) mkdirSync(join(out, `stream.log`), { recursive: true })
const started = Date.now()
const child = spawnSync(process.execPath, args, {
cwd: repo,
encoding: `utf8`,
timeout: 300_000,
maxBuffer: 64 * 1024 * 1024,
env: cleanEnv(),
})
logRun({
via: `runCli`,
dir: basename(dir),
ms: Date.now() - started,
status: child.status,
signal: child.signal,
killedByTimeout: child.error?.code === `ETIMEDOUT`,
daemon: how.daemon === true,
})
const run = readRun(
dir,
out,
Expand Down Expand Up @@ -730,11 +749,10 @@ async function stoppedRun(
const atSpawn = when === `spawn`
const after = when === `after`
const warm = when === `warm`
// With `abort`, no signal comes: a test of file a aborts the run's
// process (`GAP_ABORT`), in worker threads, so the abort ends the process
// that runs the check, as a V8 out-of-memory abort does. The run then
// ends with one error line per stream that names how its process ended,
// and exits 3 (the coordinator's decision of 2026-11-12 on P10).
// With `abort`, no signal comes: the check's process aborts in its main
// thread (ORACLE_GAPS_TEST_ABORT_AT, as a V8 out-of-memory abort does). The
// run then ends with one error line per stream that names how its process
// ended, and exits 3 (the coordinator's decision of 2026-11-12 on P10).
const abort = when === `abort`
writeFixture(dir, files)
const before = new Set(filesUnder(dir))
Expand Down Expand Up @@ -765,7 +783,7 @@ async function stoppedRun(
ORACLE_GAPS_TEST_WARM_PROBE: join(pids, `warming`),
}
: abort
? { ...cleanEnv(), GAP_ABORT: `1` }
? { ...cleanEnv(), ORACLE_GAPS_TEST_ABORT_AT: `check` }
: { ...cleanEnv(), GAP_STOP_DIR: pids }),
},
stdio: [`ignore`, `pipe`, `pipe`],
Expand Down Expand Up @@ -1075,10 +1093,23 @@ async function runClisAtOnce(dir, files, hows) {
child.stderr.on(`data`, (chunk) => {
stderr += chunk
})
const started = Date.now()
let timedOut = false
const status = await new Promise((resolvePromise) => {
const timer = setTimeout(() => child.kill(), 300_000)
const timer = setTimeout(() => {
timedOut = true
child.kill()
}, 300_000)
child.on(`close`, (code) => {
clearTimeout(timer)
logRun({
via: `runClisAtOnce`,
dir: basename(dir),
ms: Date.now() - started,
status: code,
killedByTimeout: timedOut,
daemon: how.daemon === true,
})
resolvePromise(code)
})
})
Expand Down Expand Up @@ -2758,7 +2789,10 @@ function check(plan, observations, expected) {
value.when === `after`
? `oracle-gaps: done completed`
: value.when === `abort`
? `oracle-gaps: done error, the run's process exited with SIGABRT`
? process.platform === `win32`
? // win32 has no signal a process can die of: the exit code names it.
`oracle-gaps: done error, the run's process exited with code `
: `oracle-gaps: done error, the run's process exited with SIGABRT`
: `oracle-gaps: done stopped (${value.signal})`
if (value.spun && value.when === `warm` && value.refused)
return violation(
Expand Down Expand Up @@ -2877,7 +2911,9 @@ function streamBreach(checkpoint, run, expected, plan) {
}

/** The line that names a run's stream log. */
const logNoteOf = (run) => `oracle-gaps: note log ${run.outDir}/stream.log`
// The note names the log in forward slashes (the Windows paths rule).
const logNoteOf = (run) =>
`oracle-gaps: note log ${run.outDir.replaceAll(`\\`, `/`)}/stream.log`

/**
* What law 15 judges of every run, on both streams, whatever its outcome:
Expand Down
Loading
Loading