From 4d70c2859a16f40de289021953c857cc24eb514b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 6 Oct 2026 09:03:05 -0400 Subject: [PATCH] test: stop CI test runs from timing out The bugc example tests never let the worker's event loop turn, so on a slow runner vitest timed out waiting on its own progress reports. Yield after each test, and validate each distinct instruction context once instead of re-validating every copy. --- packages/bugc/test/examples/examples.test.ts | 77 +++++++++++++++++--- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/bugc/test/examples/examples.test.ts b/packages/bugc/test/examples/examples.test.ts index f82b0d3554..0714c1ad41 100644 --- a/packages/bugc/test/examples/examples.test.ts +++ b/packages/bugc/test/examples/examples.test.ts @@ -14,12 +14,16 @@ * Supports fenced YAML test blocks (see annotations.ts for format). */ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, afterEach } from "vitest"; import { promises as fs } from "fs"; import path from "path"; import { glob } from "glob"; import * as Format from "@ethdebug/format"; -import { addSchema, validate } from "@hyperjump/json-schema/draft-2020-12"; +import { + addSchema, + validate, + type Validator, +} from "@hyperjump/json-schema/draft-2020-12"; import { BASIC } from "@hyperjump/json-schema/experimental"; import { bytecodeSequence, buildSequence } from "#compiler"; @@ -67,6 +71,57 @@ interface ExampleInfo { testBlocks: TestBlock[]; } +// Each instruction's context is a $ref to the context schema, so it +// validates on its own. Most contexts repeat, across a program's +// instructions and across optimization levels, and contexts make up +// nearly all of a program's size. So validate each distinct context +// once, and validate the rest of the program without them. +const contextErrors = new Map(); + +async function invalidLocations( + schema: string, + value: Parameters[0], +): Promise { + const output = await validate(schema, value, BASIC); + return output.valid + ? [] + : (output.errors ?? []).map((e) => e.instanceLocation); +} + +async function invalidProgramLocations( + program: Format.Program, +): Promise { + // validate the program as it is written out, as JSON + const { instructions, ...rest } = JSON.parse(JSON.stringify(program)); + const invalid = await invalidLocations("schema:ethdebug/format/program", { + ...rest, + instructions: instructions.map( + ({ context: _, ...instruction }: Format.Program.Instruction) => + instruction, + ), + }); + + for (const [index, { context }] of instructions.entries()) { + if (context === undefined) continue; + const key = JSON.stringify(context); + let errors = contextErrors.get(key); + if (!errors) { + errors = await invalidLocations( + "schema:ethdebug/format/program/context", + context, + ); + contextErrors.set(key, errors); + } + invalid.push( + ...errors.map( + (location) => `#/instructions/${index}/context${location.slice(1)}`, + ), + ); + } + + return invalid; +} + // Cache for compiled examples const compilationCache = new Map< string, @@ -131,6 +186,13 @@ async function compileExample( return cacheEntry; } +// The compiler and the schema validator do all their work synchronously, +// so this file can run start to end without the event loop ever turning. +// The worker then cannot read vitest's replies to its progress reports, +// and once that lasts a minute, vitest fails the run with 'Timeout +// calling "onTaskUpdate"'. Give the event loop a turn after each test. +afterEach(() => new Promise((resolve) => setImmediate(resolve))); + describe("Example Files", async () => { const examples = await loadExamples(); @@ -224,16 +286,7 @@ describe("Example Files", async () => { ); for (const program of [runtimeProgram, createProgram]) { if (!program) continue; - // validate the program as it is written out, as JSON - const output = await validate( - "schema:ethdebug/format/program", - JSON.parse(JSON.stringify(program)), - BASIC, - ); - const invalid = output.valid - ? [] - : (output.errors ?? []).map((e) => e.instanceLocation); - expect(invalid).toEqual([]); + expect(await invalidProgramLocations(program)).toEqual([]); // every storage variable is in the program-level context const { context } = program;