v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,886 @@
|
||||
/**
|
||||
* Integration tests for the property runner.
|
||||
*
|
||||
* Mocks the renderer adapter and scene target so these tests exercise
|
||||
* the runner orchestration without needing a real browser.
|
||||
*
|
||||
* Pattern: Arrange-Act-Assert with Node.js built-in test runner.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
|
||||
import type { SceneTarget, RenderCase } from 'imhotep-core/scene-target'
|
||||
import type { InputDomain } from 'imhotep-core/property-contracts'
|
||||
import type { GeometryWorld } from 'imhotep-core/world'
|
||||
import type { MountedScene, RendererAdapter, PropertyPredicate, PropertyRunnerDeps } from './property-runner.js'
|
||||
import { runProperty, runEnumeratedProperty, runSampledProperty } from './property-runner.js'
|
||||
import type { FastCheckAdapter } from './fast-check-adapter.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock Builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMockRendererAdapter(
|
||||
opts: { failOnCaseId?: string } = {},
|
||||
): RendererAdapter {
|
||||
const mountedScenes: MountedScene[] = []
|
||||
|
||||
return {
|
||||
async mount(_sceneTarget: SceneTarget, renderCase: RenderCase): Promise<MountedScene> {
|
||||
if (opts.failOnCaseId && renderCase.caseId === opts.failOnCaseId) {
|
||||
throw new Error(`Simulated mount failure for ${renderCase.caseId}`)
|
||||
}
|
||||
const scene: MountedScene = {
|
||||
caseId: renderCase.caseId,
|
||||
world: createMockWorld(renderCase.caseId),
|
||||
}
|
||||
mountedScenes.push(scene)
|
||||
return scene
|
||||
},
|
||||
|
||||
async unmount(mounted: MountedScene): Promise<void> {
|
||||
const idx = mountedScenes.findIndex((s) => s.caseId === mounted.caseId)
|
||||
if (idx >= 0) mountedScenes.splice(idx, 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createMockWorld(sceneId: string): GeometryWorld {
|
||||
return {
|
||||
sceneId,
|
||||
snapshotId: 'snap-1',
|
||||
env: {
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
deviceScaleFactor: 1,
|
||||
colorScheme: 'light',
|
||||
pointer: 'fine',
|
||||
hover: false,
|
||||
reducedMotion: 'no-preference',
|
||||
locale: 'en',
|
||||
writingMode: 'horizontal-tb',
|
||||
},
|
||||
source: {
|
||||
url: 'http://localhost/test',
|
||||
browserName: 'chromium',
|
||||
browserVersion: '120',
|
||||
engine: 'chromium-cdp',
|
||||
},
|
||||
strings: [],
|
||||
subjects: [],
|
||||
dom: { nodes: [], nodeCount: 0 },
|
||||
frames: [],
|
||||
matrices: [],
|
||||
rects: [],
|
||||
boxes: [],
|
||||
fragments: [],
|
||||
transforms: [],
|
||||
styles: [],
|
||||
text: [],
|
||||
topology: { ancestors: [], descendants: [], stacks: [] },
|
||||
scroll: [],
|
||||
clipping: [],
|
||||
paint: [],
|
||||
visibility: [],
|
||||
provenance: [],
|
||||
confidence: [],
|
||||
} as unknown as GeometryWorld
|
||||
}
|
||||
|
||||
function createMockFastCheckAdapter(): FastCheckAdapter {
|
||||
return {
|
||||
assert: async (prop: unknown, params?: Record<string, unknown>) => {
|
||||
const p = prop as { predicate: (value: unknown) => boolean | Promise<boolean>; arbitrary: unknown }
|
||||
const numRuns = (params?.numRuns as number) ?? 100
|
||||
const seed = (params?.seed as number) ?? 42
|
||||
|
||||
// Deterministic pseudo-random from seed for replayability
|
||||
let rngState = seed
|
||||
const next = () => {
|
||||
rngState = (rngState * 16807 + 0) % 2147483647
|
||||
return rngState / 2147483647
|
||||
}
|
||||
|
||||
for (let i = 0; i < numRuns; i++) {
|
||||
const value = generateValue(p.arbitrary, next)
|
||||
const passed = await p.predicate(value)
|
||||
if (!passed) {
|
||||
const err = new Error(`Property failed at run ${i}`)
|
||||
;(err as unknown as Record<string, unknown>).counterexample = value
|
||||
throw err
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
record: (recordModel: Record<string, unknown>) => recordModel,
|
||||
|
||||
constantFrom: (...values: unknown[]) => values,
|
||||
|
||||
sample: (arb: unknown, params?: { seed?: number; numRuns?: number }) => {
|
||||
const numRuns = params?.numRuns ?? 10
|
||||
const seed = params?.seed ?? 42
|
||||
let rngState = seed
|
||||
const next = () => {
|
||||
rngState = (rngState * 16807 + 0) % 2147483647
|
||||
return rngState / 2147483647
|
||||
}
|
||||
const out: unknown[] = []
|
||||
for (let i = 0; i < numRuns; i++) {
|
||||
out.push(generateValue(arb, next))
|
||||
}
|
||||
return out
|
||||
},
|
||||
|
||||
property: (arb: unknown, predicate: (value: unknown) => boolean | Promise<boolean>) => {
|
||||
return { arbitrary: arb, predicate }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function generateValue(arb: unknown, next: () => number): unknown {
|
||||
if (Array.isArray(arb)) {
|
||||
const idx = Math.floor(next() * arb.length)
|
||||
return arb[idx]
|
||||
}
|
||||
if (arb && typeof arb === 'object') {
|
||||
const obj: Record<string, unknown> = {}
|
||||
for (const [key, val] of Object.entries(arb)) {
|
||||
obj[key] = generateValue(val, next)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return arb
|
||||
}
|
||||
|
||||
function createConcurrencyTrackingAdapter(delayMs = 50) {
|
||||
let currentConcurrent = 0
|
||||
let maxConcurrent = 0
|
||||
|
||||
const adapter: RendererAdapter = {
|
||||
async mount(_sceneTarget: SceneTarget, renderCase: RenderCase): Promise<MountedScene> {
|
||||
currentConcurrent++
|
||||
maxConcurrent = Math.max(maxConcurrent, currentConcurrent)
|
||||
await new Promise((r) => setTimeout(r, delayMs))
|
||||
return {
|
||||
caseId: renderCase.caseId,
|
||||
world: createMockWorld(renderCase.caseId),
|
||||
}
|
||||
},
|
||||
async unmount(): Promise<void> {
|
||||
currentConcurrent--
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
adapter,
|
||||
getMaxConcurrent: () => maxConcurrent,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const sceneTarget: SceneTarget = { kind: 'fixture', fixtureId: 'button-fixture' }
|
||||
|
||||
describe('property runner — enumerated mode', () => {
|
||||
it('evaluates every enumerated case deterministically', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = ['sm', 'md', 'lg']
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const evaluated: string[] = []
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
evaluated.push((scene as MountedScene).caseId)
|
||||
return true
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps)
|
||||
|
||||
assert.strictEqual(result.mode, 'enumerated-determinate')
|
||||
assert.strictEqual(result.totalCases, 3)
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.strictEqual(evaluated.length, 3)
|
||||
assert.ok(evaluated.includes('enum-0'))
|
||||
assert.ok(evaluated.includes('enum-1'))
|
||||
assert.ok(evaluated.includes('enum-2'))
|
||||
})
|
||||
|
||||
it('stops at first failing case and reports it', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = ['sm', 'md', 'lg']
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
return (scene as MountedScene).caseId !== 'enum-1'
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps)
|
||||
|
||||
assert.strictEqual(result.mode, 'enumerated-determinate')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.failingCase, 'md')
|
||||
assert.strictEqual(result.caseIndex, 1)
|
||||
})
|
||||
|
||||
it('enumerated runs all cases concurrently', async () => {
|
||||
const { adapter } = createConcurrencyTrackingAdapter(50)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = [1, 2, 3, 4, 5]
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const start = performance.now()
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, async () => true, deps, {
|
||||
concurrency: 5,
|
||||
})
|
||||
const elapsed = performance.now() - start
|
||||
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.strictEqual(result.totalCases, 5)
|
||||
assert.ok(elapsed < 200, `Expected concurrent run to be fast, but took ${elapsed}ms`)
|
||||
})
|
||||
|
||||
it('enumerated stops on first failure with failFast', async () => {
|
||||
const { adapter } = createConcurrencyTrackingAdapter(10)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = [1, 2, 3, 4, 5]
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const evaluated: number[] = []
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
const idx = parseInt((scene as MountedScene).caseId.replace('enum-', ''))
|
||||
evaluated.push(idx)
|
||||
return idx !== 2
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps, {
|
||||
concurrency: 1,
|
||||
failFast: true,
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 2)
|
||||
assert.ok(evaluated.includes(0))
|
||||
assert.ok(evaluated.includes(1))
|
||||
assert.ok(evaluated.includes(2))
|
||||
assert.ok(!evaluated.includes(3))
|
||||
assert.ok(!evaluated.includes(4))
|
||||
})
|
||||
|
||||
it('enumerated runs all cases despite failure without failFast', async () => {
|
||||
const { adapter } = createConcurrencyTrackingAdapter(10)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = [1, 2, 3, 4, 5]
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const evaluated: number[] = []
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
const idx = parseInt((scene as MountedScene).caseId.replace('enum-', ''))
|
||||
evaluated.push(idx)
|
||||
return idx !== 2
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps, {
|
||||
concurrency: 1,
|
||||
failFast: false,
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 2)
|
||||
assert.strictEqual(evaluated.length, 5)
|
||||
assert.ok(evaluated.includes(0))
|
||||
assert.ok(evaluated.includes(1))
|
||||
assert.ok(evaluated.includes(2))
|
||||
assert.ok(evaluated.includes(3))
|
||||
assert.ok(evaluated.includes(4))
|
||||
})
|
||||
|
||||
it('enumerated respects concurrency limit', async () => {
|
||||
const { adapter, getMaxConcurrent } = createConcurrencyTrackingAdapter(50)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = [1, 2, 3, 4, 5]
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, async () => true, deps, {
|
||||
concurrency: 2,
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.ok(getMaxConcurrent() <= 2, `Expected max concurrent <= 2, but got ${getMaxConcurrent()}`)
|
||||
assert.ok(getMaxConcurrent() > 1, `Expected some concurrency, but got ${getMaxConcurrent()}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('property runner — sampled mode', () => {
|
||||
it('runs sampled cases with fast-check and reports seed', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
}
|
||||
|
||||
let runCount = 0
|
||||
const predicate: PropertyPredicate = async () => {
|
||||
runCount++
|
||||
return true
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, predicate, deps, { seed: 42, numRuns: 20 })
|
||||
|
||||
assert.strictEqual(result.mode, 'sampled')
|
||||
assert.strictEqual(result.seed, 42)
|
||||
assert.strictEqual(result.numRuns, 20)
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.strictEqual(runCount, 20)
|
||||
})
|
||||
|
||||
it('captures counterexample on failure', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const predicate: PropertyPredicate = async () => {
|
||||
callCount++
|
||||
return callCount < 5
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, predicate, deps, { seed: 42, numRuns: 20 })
|
||||
|
||||
assert.strictEqual(result.mode, 'sampled')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.ok(result.counterexample !== undefined)
|
||||
assert.ok(result.diagnostics.length > 0)
|
||||
})
|
||||
|
||||
it('reproduces identical run with same seed', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 123,
|
||||
numRuns: 10,
|
||||
}
|
||||
|
||||
const run1Inputs: unknown[] = []
|
||||
const run2Inputs: unknown[] = []
|
||||
|
||||
const predicate1: PropertyPredicate = async (_scene, ctx) => {
|
||||
run1Inputs.push(ctx.renderCase.input)
|
||||
return true
|
||||
}
|
||||
|
||||
const predicate2: PropertyPredicate = async (_scene, ctx) => {
|
||||
run2Inputs.push(ctx.renderCase.input)
|
||||
return true
|
||||
}
|
||||
|
||||
await runSampledProperty(sceneTarget, domain, predicate1, deps, { seed: 123, numRuns: 10 })
|
||||
await runSampledProperty(sceneTarget, domain, predicate2, deps, { seed: 123, numRuns: 10 })
|
||||
|
||||
assert.deepStrictEqual(run1Inputs, run2Inputs)
|
||||
})
|
||||
|
||||
it('sampled runs iterations concurrently', async () => {
|
||||
const { adapter } = createConcurrencyTrackingAdapter(50)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('a', 'b', 'c'),
|
||||
seed: 42,
|
||||
numRuns: 10,
|
||||
}
|
||||
|
||||
const start = performance.now()
|
||||
const result = await runSampledProperty(sceneTarget, domain, async () => true, deps, {
|
||||
seed: 42,
|
||||
numRuns: 10,
|
||||
concurrency: 10,
|
||||
})
|
||||
const elapsed = performance.now() - start
|
||||
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.strictEqual(result.numRuns, 10)
|
||||
assert.ok(elapsed < 300, `Expected concurrent sampled run to be fast, but took ${elapsed}ms`)
|
||||
})
|
||||
|
||||
it('sampled respects concurrency limit', async () => {
|
||||
const { adapter, getMaxConcurrent } = createConcurrencyTrackingAdapter(50)
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('a', 'b', 'c'),
|
||||
seed: 42,
|
||||
numRuns: 10,
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, async () => true, deps, {
|
||||
seed: 42,
|
||||
numRuns: 10,
|
||||
concurrency: 2,
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.ok(getMaxConcurrent() <= 2, `Expected max concurrent <= 2, but got ${getMaxConcurrent()}`)
|
||||
assert.ok(getMaxConcurrent() > 1, `Expected some concurrency, but got ${getMaxConcurrent()}`)
|
||||
})
|
||||
|
||||
it('sampled preserves seed and shrinking', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const predicate: PropertyPredicate = async () => {
|
||||
callCount++
|
||||
return callCount < 5
|
||||
}
|
||||
|
||||
const result1 = await runSampledProperty(sceneTarget, domain, predicate, deps, {
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
})
|
||||
|
||||
callCount = 0
|
||||
const result2 = await runSampledProperty(sceneTarget, domain, predicate, deps, {
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
})
|
||||
|
||||
assert.strictEqual(result1.passed, false)
|
||||
assert.strictEqual(result2.passed, false)
|
||||
assert.deepStrictEqual(result1.counterexample, result2.counterexample)
|
||||
assert.deepStrictEqual(result1.minimalFailingCase, result2.minimalFailingCase)
|
||||
})
|
||||
})
|
||||
|
||||
describe('property runner — error handling', () => {
|
||||
it('error in one case does not crash runner', async () => {
|
||||
const evaluated: number[] = []
|
||||
const adapter: RendererAdapter = {
|
||||
async mount(_sceneTarget, renderCase) {
|
||||
if (renderCase.caseId === 'enum-2') {
|
||||
throw new Error('Mount failed')
|
||||
}
|
||||
return {
|
||||
caseId: renderCase.caseId,
|
||||
world: createMockWorld(renderCase.caseId),
|
||||
}
|
||||
},
|
||||
async unmount() {},
|
||||
}
|
||||
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = [1, 2, 3, 4, 5]
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
const idx = parseInt((scene as MountedScene).caseId.replace('enum-', ''))
|
||||
evaluated.push(idx)
|
||||
return true
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps, {
|
||||
failFast: false,
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 2)
|
||||
assert.ok(evaluated.includes(0))
|
||||
assert.ok(evaluated.includes(1))
|
||||
assert.ok(evaluated.includes(3))
|
||||
assert.ok(evaluated.includes(4))
|
||||
})
|
||||
})
|
||||
|
||||
describe('property runner — mount/unmount', () => {
|
||||
it('mount/unmount called for every case', async () => {
|
||||
let mountCount = 0
|
||||
let unmountCount = 0
|
||||
|
||||
const adapter: RendererAdapter = {
|
||||
async mount(_sceneTarget, renderCase) {
|
||||
mountCount++
|
||||
return {
|
||||
caseId: renderCase.caseId,
|
||||
world: createMockWorld(renderCase.caseId),
|
||||
}
|
||||
},
|
||||
async unmount() {
|
||||
unmountCount++
|
||||
},
|
||||
}
|
||||
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = ['a', 'b', 'c', 'd', 'e']
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, async () => true, deps)
|
||||
|
||||
assert.strictEqual(result.passed, true)
|
||||
assert.strictEqual(mountCount, 5)
|
||||
assert.strictEqual(unmountCount, 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('property runner — dispatch', () => {
|
||||
it('dispatches to enumerated runner for enumerated domain', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = { mode: 'enumerated', values: [1, 2, 3] }
|
||||
const result = await runProperty(sceneTarget, domain, async () => true, deps)
|
||||
|
||||
assert.strictEqual(result.mode, 'enumerated-determinate')
|
||||
})
|
||||
|
||||
it('dispatches to sampled runner for generated domain', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('a', 'b'),
|
||||
seed: 1,
|
||||
numRuns: 5,
|
||||
}
|
||||
const result = await runProperty(sceneTarget, domain, async () => true, deps, { seed: 1, numRuns: 5 })
|
||||
|
||||
assert.strictEqual(result.mode, 'sampled')
|
||||
})
|
||||
|
||||
it('throws for unsupported domain mode', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain = { mode: 'unknown' } as unknown as InputDomain
|
||||
await assert.rejects(
|
||||
async () => runProperty(sceneTarget, domain, async () => true, deps),
|
||||
/Unsupported InputDomain mode/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('property runner — report mode', () => {
|
||||
it('compact enumerated result strips verbose fields', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = ['sm', 'md', 'lg']
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
return (scene as MountedScene).caseId !== 'enum-1'
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps, {
|
||||
reportMode: 'compact',
|
||||
})
|
||||
|
||||
assert.strictEqual(result.mode, 'enumerated-determinate')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 1)
|
||||
assert.strictEqual(result.minimalFailingCase, 'md')
|
||||
assert.ok(result.durationMs !== undefined)
|
||||
assert.ok(result.replayPayload !== undefined)
|
||||
assert.deepStrictEqual(result.replayPayload!, {
|
||||
props: 'md',
|
||||
seed: -1,
|
||||
caseIndex: 1,
|
||||
})
|
||||
assert.strictEqual(result.diagnostics.length, 0)
|
||||
assert.strictEqual((result as any).failingScene, undefined)
|
||||
})
|
||||
|
||||
it('verbose enumerated result includes full payload', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const values = ['sm', 'md', 'lg']
|
||||
const domain: InputDomain = { mode: 'enumerated', values }
|
||||
|
||||
const predicate: PropertyPredicate = async (scene) => {
|
||||
return (scene as MountedScene).caseId !== 'enum-1'
|
||||
}
|
||||
|
||||
const result = await runEnumeratedProperty(sceneTarget, domain, predicate, deps, {
|
||||
reportMode: 'verbose',
|
||||
})
|
||||
|
||||
assert.strictEqual(result.mode, 'enumerated-determinate')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 1)
|
||||
assert.strictEqual(result.minimalFailingCase, 'md')
|
||||
assert.ok(result.durationMs !== undefined)
|
||||
assert.ok(Array.isArray(result.diagnostics))
|
||||
assert.ok(result.replayPayload !== undefined)
|
||||
assert.deepStrictEqual(result.replayPayload!, {
|
||||
props: 'md',
|
||||
seed: -1,
|
||||
caseIndex: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('compact sampled result strips verbose fields', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const predicate: PropertyPredicate = async () => {
|
||||
callCount++
|
||||
return callCount < 5
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, predicate, deps, {
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
reportMode: 'compact',
|
||||
})
|
||||
|
||||
assert.strictEqual(result.mode, 'sampled')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 4)
|
||||
assert.ok(result.minimalFailingCase !== undefined)
|
||||
assert.ok(result.durationMs !== undefined)
|
||||
assert.ok(result.replayPayload !== undefined)
|
||||
assert.strictEqual(result.replayPayload!.seed, 42)
|
||||
assert.strictEqual(result.replayPayload!.caseIndex, 4)
|
||||
assert.ok(result.replayPayload!.props !== undefined)
|
||||
assert.strictEqual(result.diagnostics.length, 0)
|
||||
assert.strictEqual((result as any).failingScene, undefined)
|
||||
})
|
||||
|
||||
it('verbose sampled result includes full payload', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const predicate: PropertyPredicate = async () => {
|
||||
callCount++
|
||||
return callCount < 5
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, predicate, deps, {
|
||||
seed: 42,
|
||||
numRuns: 20,
|
||||
reportMode: 'verbose',
|
||||
})
|
||||
|
||||
assert.strictEqual(result.mode, 'sampled')
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.strictEqual(result.caseIndex, 4)
|
||||
assert.ok(result.minimalFailingCase !== undefined)
|
||||
assert.ok(result.durationMs !== undefined)
|
||||
assert.ok(Array.isArray(result.diagnostics))
|
||||
assert.ok(result.replayPayload !== undefined)
|
||||
assert.strictEqual(result.replayPayload!.seed, 42)
|
||||
assert.strictEqual(result.replayPayload!.caseIndex, 4)
|
||||
assert.ok(result.replayPayload!.props !== undefined)
|
||||
})
|
||||
|
||||
it('reproduces identical run from sampled replay payload', async () => {
|
||||
const adapter = createMockRendererAdapter()
|
||||
const fc = createMockFastCheckAdapter()
|
||||
const deps: PropertyRunnerDeps = {
|
||||
rendererAdapter: adapter,
|
||||
worldExtractor: { extract: async (m) => m.world },
|
||||
fc,
|
||||
}
|
||||
|
||||
const domain: InputDomain = {
|
||||
mode: 'generated',
|
||||
arbitrary: fc.constantFrom('sm', 'md', 'lg'),
|
||||
seed: 123,
|
||||
numRuns: 10,
|
||||
}
|
||||
|
||||
let callCount = 0
|
||||
const collectedInputs: unknown[] = []
|
||||
const predicate: PropertyPredicate = async (_scene, ctx) => {
|
||||
collectedInputs.push(ctx.renderCase.input)
|
||||
callCount++
|
||||
return callCount < 7
|
||||
}
|
||||
|
||||
const result = await runSampledProperty(sceneTarget, domain, predicate, deps, {
|
||||
seed: 123,
|
||||
numRuns: 10,
|
||||
reportMode: 'verbose',
|
||||
})
|
||||
|
||||
assert.strictEqual(result.passed, false)
|
||||
assert.ok(result.replayPayload !== undefined)
|
||||
|
||||
const replayInputs: unknown[] = []
|
||||
let replayCount = 0
|
||||
const replayPayload = result.replayPayload!
|
||||
const replayPredicate: PropertyPredicate = async (_scene, ctx) => {
|
||||
replayInputs.push(ctx.renderCase.input)
|
||||
replayCount++
|
||||
return replayCount < replayPayload.caseIndex + 1
|
||||
}
|
||||
|
||||
await runSampledProperty(sceneTarget, domain, replayPredicate, deps, {
|
||||
seed: replayPayload.seed,
|
||||
numRuns: replayPayload.caseIndex + 1,
|
||||
reportMode: 'verbose',
|
||||
})
|
||||
|
||||
assert.deepStrictEqual(
|
||||
collectedInputs[replayPayload.caseIndex],
|
||||
replayInputs[replayPayload.caseIndex],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user