import { times } from 'lodash' import { Atom, atom, isAtom } from '../Atom' import { Computed, computed, isComputed } from '../Computed' import { Reactor, reactor } from '../EffectScheduler' import { transact } from '../transactions' import { Signal } from '../types' class RandomSource { private seed: number constructor(seed: number) { this.seed = seed } nextFloat(): number { this.seed = (this.seed * 9301 + 49297) % 233280 return this.seed / 233280 } nextInt(max: number): number { return Math.floor(this.nextFloat() * max) } nextIntInRange(min: number, max: number): number { return this.nextInt(max - min) + min } nextId(): string { return this.nextInt(Number.MAX_SAFE_INTEGER).toString(36) } selectOne(arr: readonly T[]): T { return arr[this.nextInt(arr.length)] } choice(probability: number): boolean { return this.nextFloat() < probability } executeOne( _choices: Record Result) | { weight?: number; do(): Result }> ): Result { const choices = Object.values(_choices).map((choice) => { if (typeof choice === 'function') { return { weight: 1, do: choice } } return choice }) const totalWeight = Object.values(choices).reduce( (total, choice) => total + (choice.weight ?? 1), 0 ) const randomWeight = this.nextInt(totalWeight) let weight = 0 for (const choice of Object.values(choices)) { weight += choice.weight ?? 1 if (randomWeight < weight) { return choice.do() } } throw new Error('unreachable') } } const LETTERS = ['a', 'b', 'c', 'd', 'e', 'f'] as const type Letter = (typeof LETTERS)[number] const unpack = (value: unknown): Letter => { if (isComputed(value) || isAtom(value)) { return unpack(value.get()) as Letter } return value as Letter } type FuzzSystemState = { atoms: Record> atomsInAtoms: Record>> derivations: Record; sneakyGet: () => Letter }> derivationsInDerivations: Record>> atomsInDerivations: Record>> reactors: Record[] }> } type Op = | { type: 'update_atom'; id: string; value: Letter } | { type: 'update_atom_in_atom'; id: string; atomId: string } | { type: 'deref_derivation'; id: string } | { type: 'deref_derivation_in_derivation'; id: string } | { type: 'deref_atom_in_derivation'; id: string } | { type: 'run_several_ops_in_transaction'; ops: Op[] } | { type: 'start_reactor'; id: string } | { type: 'stop_reactor'; id: string } const MAX_ATOMS = 10 const MAX_ATOMS_IN_ATOMS = 10 const MAX_DERIVATIONS = 10 const MAX_DERIVATIONS_IN_DERIVATIONS = 10 const MAX_ATOMS_IN_DERIVATIONS = 10 const MAX_REACTORS = 10 const MAX_DEPENDENCIES_PER_ATOM = 3 const MAX_OPS_IN_TRANSACTION = 10 class Test { source: RandomSource systemState: FuzzSystemState = { atoms: {}, atomsInAtoms: {}, derivations: {}, derivationsInDerivations: {}, atomsInDerivations: {}, reactors: {}, } unpack_sneaky = (value: unknown): Letter => { if (isComputed(value)) { if (this.systemState.derivations[value.name]) { return this.systemState.derivations[value.name].sneakyGet() } // @ts-expect-error return this.unpack_sneaky(value.state) as Letter } else if (isAtom(value)) { // @ts-expect-error return this.unpack_sneaky(value.current) as Letter } return value as Letter } getResultComparisons() { const result: { expected: Record; actual: Record } = { expected: {}, actual: {}, } for (const [reactorId, { reactor, result: actualResult, dependencies }] of Object.entries( this.systemState.reactors )) { if (!reactor.scheduler.isActivelyListening) continue result.expected[reactorId] = dependencies.map(this.unpack_sneaky).join(':') result.actual[reactorId] = actualResult } return result } constructor(seed: number) { this.source = new RandomSource(seed) times(this.source.nextIntInRange(1, MAX_ATOMS), () => { const atomId = this.source.nextId() this.systemState.atoms[atomId] = atom(atomId, this.source.selectOne(LETTERS)) }) times(this.source.nextIntInRange(1, MAX_ATOMS_IN_ATOMS), () => { const atomId = this.source.nextId() this.systemState.atomsInAtoms[atomId] = atom( atomId, this.source.selectOne(Object.values(this.systemState.atoms)) ) }) times(this.source.nextIntInRange(1, MAX_ATOMS_IN_DERIVATIONS), () => { const derivationId = this.source.nextId() const atom = this.source.selectOne(Object.values(this.systemState.atoms)) this.systemState.atomsInDerivations[derivationId] = computed(derivationId, () => atom) }) times(this.source.nextIntInRange(1, MAX_DERIVATIONS), () => { const derivationId = this.source.nextId() const derivables = [ ...Object.values(this.systemState.atoms), ...Object.values(this.systemState.atomsInAtoms), ...Object.values(this.systemState.atomsInDerivations), ...Object.values(this.systemState.derivations), ] const inputA = this.source.selectOne(derivables) const inputB = this.source.selectOne(derivables) const inputC = this.source.selectOne(derivables) const inputD = this.source.selectOne(derivables) this.systemState.derivations[derivationId] = { derivation: computed(derivationId, () => { if (unpack(inputA) === unpack(inputB)) { return unpack(inputC) } else { return unpack(inputD) } }), sneakyGet: () => { if (this.unpack_sneaky(inputA) === this.unpack_sneaky(inputB)) { return this.unpack_sneaky(inputC) } else { return this.unpack_sneaky(inputD) } }, } }) times(this.source.nextIntInRange(1, MAX_DERIVATIONS_IN_DERIVATIONS), () => { const derivationId = this.source.nextId() this.systemState.derivationsInDerivations[derivationId] = computed(derivationId, () => this.source.selectOne(Object.values(this.systemState.derivations).map((d) => d.derivation)) ) }) times(this.source.nextIntInRange(1, MAX_REACTORS), () => { const reactorId = this.source.nextId() const dependencies: Signal[] = [] times(this.source.nextIntInRange(1, MAX_DEPENDENCIES_PER_ATOM), () => { this.source.executeOne({ 'add a random atom': () => { dependencies.push(this.source.selectOne(Object.values(this.systemState.atoms))) }, 'add a random atom in atom': () => { dependencies.push(this.source.selectOne(Object.values(this.systemState.atomsInAtoms))) }, 'add a random derivation': () => { dependencies.push( this.source.selectOne( Object.values(this.systemState.derivations).map((d) => d.derivation) ) ) }, 'add a random derivation in derivation': () => { dependencies.push( this.source.selectOne(Object.values(this.systemState.derivationsInDerivations)) ) }, 'add a random atom in derivation': () => { dependencies.push( this.source.selectOne(Object.values(this.systemState.atomsInDerivations)) ) }, }) dependencies.push(this.source.selectOne(Object.values(this.systemState.atoms))) }) this.systemState.reactors[reactorId] = { reactor: reactor(reactorId, () => { this.systemState.reactors[reactorId].result = dependencies.map(unpack).join(':') }), result: '', dependencies, } }) } readonly ops: Op[] = [] getNextOp(): Op { return this.source.executeOne({ 'update atom': () => { return { type: 'update_atom', id: this.source.selectOne(Object.keys(this.systemState.atoms)), value: this.source.selectOne(LETTERS), } }, 'update atom in atom': () => { return { type: 'update_atom_in_atom', id: this.source.selectOne(Object.keys(this.systemState.atomsInAtoms)), atomId: this.source.selectOne(Object.keys(this.systemState.atoms)), } }, 'deref atom in derivation': () => { return { type: 'deref_atom_in_derivation', id: this.source.selectOne(Object.keys(this.systemState.atomsInDerivations)), } }, 'deref derivation in derivation': () => { return { type: 'deref_derivation_in_derivation', id: this.source.selectOne(Object.keys(this.systemState.derivationsInDerivations)), } }, 'deref derivation': () => { return { type: 'deref_derivation', id: this.source.selectOne(Object.keys(this.systemState.derivations)), } }, 'run several ops in a transaction': () => { return { type: 'run_several_ops_in_transaction', ops: times(this.source.nextIntInRange(2, MAX_OPS_IN_TRANSACTION), () => this.getNextOp()), } }, start_reactor: () => { return { type: 'start_reactor', id: this.source.selectOne(Object.keys(this.systemState.reactors)), } }, stop_reactor: () => { return { type: 'stop_reactor', id: this.source.selectOne(Object.keys(this.systemState.reactors)), } }, }) } applyOp(op: Op) { switch (op.type) { case 'update_atom': { this.systemState.atoms[op.id].set(op.value) break } case 'deref_atom_in_derivation': { this.systemState.atomsInDerivations[op.id].get() break } case 'deref_derivation': { this.systemState.derivations[op.id].derivation.get() break } case 'deref_derivation_in_derivation': { this.systemState.derivationsInDerivations[op.id].get() break } case 'update_atom_in_atom': { this.systemState.atomsInAtoms[op.id].set(this.systemState.atoms[op.atomId]) break } case 'run_several_ops_in_transaction': { transact(() => { op.ops.forEach((op) => this.applyOp(op)) }) break } case 'start_reactor': { this.systemState.reactors[op.id].reactor.start() break } case 'stop_reactor': { this.systemState.reactors[op.id].reactor.stop() break } default: { throw new Error(`Unknown op type: ${op}`) } } } tick() { const op = this.getNextOp() this.ops.push(op) this.applyOp(op) } } const NUM_TESTS = 20 const NUM_OPS_PER_TEST = 1000 function runTest(seed: number) { const test = new Test(seed) for (let i = 0; i < NUM_OPS_PER_TEST; i++) { test.tick() const { expected, actual } = test.getResultComparisons() expect(expected).toEqual(actual) } } for (let i = 0; i < NUM_TESTS; i++) { const seed = Math.floor(Math.random() * 1000000) test('fuzzzzzz ' + seed, () => { runTest(seed) }) } test('regression 728608', () => { runTest(728608) })