|
| 1 | +// Unit-lane replay of the parser fuzz regression corpus (#1414). |
| 2 | +// |
| 3 | +// The nightly lane finds cases; this replays every case it ever found, in-process and |
| 4 | +// without a watchdog, so a regression fails in seconds on a PR instead of a night later. |
| 5 | +// Cases run synchronously here on purpose: a corpus case that hangs would hang the unit |
| 6 | +// suite, which is exactly the signal (the nightly lane is where hangs are diagnosed). |
| 7 | + |
| 8 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 9 | +import { readCorpus } from './corpus.ts'; |
| 10 | +import { checkCase } from './invariant.ts'; |
| 11 | +import { FUZZ_TARGETS, getFuzzTarget } from './targets.ts'; |
| 12 | +import { generateCases } from './mutate.ts'; |
| 13 | + |
| 14 | +describe('parser fuzz regression corpus', () => { |
| 15 | + const corpus = readCorpus(); |
| 16 | + |
| 17 | + // The batch-steps parser warns on deprecated step shapes; replaying those cases would |
| 18 | + // print one warning line per case into the unit-suite output. |
| 19 | + beforeEach(() => void vi.spyOn(process.stderr, 'write').mockReturnValue(true)); |
| 20 | + afterEach(() => void vi.restoreAllMocks()); |
| 21 | + |
| 22 | + it('is non-empty and free of duplicates', () => { |
| 23 | + expect(corpus.length).toBeGreaterThan(0); |
| 24 | + const keys = corpus.map((entry) => `${entry.target}\u0000${entry.input}`); |
| 25 | + expect(new Set(keys).size).toBe(keys.length); |
| 26 | + }); |
| 27 | + |
| 28 | + it('names only known targets and explains every entry', () => { |
| 29 | + for (const entry of corpus) { |
| 30 | + expect(() => getFuzzTarget(entry.target)).not.toThrow(); |
| 31 | + expect(entry.note.trim()).not.toBe(''); |
| 32 | + } |
| 33 | + }); |
| 34 | + |
| 35 | + it.each(corpus.map((entry, index) => [index, entry] as const))( |
| 36 | + 'case %i holds the typed-AppError invariant', |
| 37 | + (_index, entry) => { |
| 38 | + expect(checkCase(getFuzzTarget(entry.target), entry.input)).toBeNull(); |
| 39 | + }, |
| 40 | + ); |
| 41 | + |
| 42 | + it.each(FUZZ_TARGETS.map((target) => [target.name, target] as const))( |
| 43 | + '%s seeds hold the invariant', |
| 44 | + (_name, target) => { |
| 45 | + for (const seed of target.seeds) expect(checkCase(target, seed)).toBeNull(); |
| 46 | + }, |
| 47 | + ); |
| 48 | +}); |
| 49 | + |
| 50 | +describe('fuzz case generation', () => { |
| 51 | + it('is deterministic for a seed, so a repro command reproduces', () => { |
| 52 | + const seeds = ['text=Login', 'label="Sign in" && role=button']; |
| 53 | + expect(generateCases(seeds, 32, 7)).toEqual(generateCases(seeds, 32, 7)); |
| 54 | + expect(generateCases(seeds, 32, 7)).not.toEqual(generateCases(seeds, 32, 8)); |
| 55 | + }); |
| 56 | + |
| 57 | + it('always covers the verbatim seeds before mutating', () => { |
| 58 | + const seeds = ['a', 'b', 'c']; |
| 59 | + expect(generateCases(seeds, 1, 1)).toEqual(seeds); |
| 60 | + expect(generateCases(seeds, 10, 1).slice(0, 3)).toEqual(seeds); |
| 61 | + }); |
| 62 | +}); |
0 commit comments