Part 4: Runners with shrink

node v14.20.1
version: 1.0.0
endpointsharetweet
Let's define our minimal framework
const prand = require('pure-rand'); class Random { constructor(rng) { this.rng = rng; } next(min, max) { const g = prand.uniformIntDistribution(min, max, this.rng); this.rng = g[1]; return g[0]; } } const map = (g, mapper, unmapper) => { return { generate(mrng) { return mapper(g.generate(mrng)); }, *shrink(value) { for (const shrunkValue of g.shrink(unmapper(value))) { yield mapper(shrunkValue); } } }; }; const miniFc = {} miniFc.integer = (min, max) => { return { generate(mrng) { return mrng.next(min, max); }, *shrink(value) { while (value !== min) { value = min + Math.floor((value - min) / 2); yield value; } } } } miniFc.tuple = (...itemGenerators) => { return { generate(mrng) { return itemGenerators.map(g => g.generate(mrng)); }, *shrink(value) { for (let index = 0 ; index !== itemGenerators.length ; ++index) { const currentGenerator = itemGenerators[index]; const currentValue = value[index]; for (const shrunkValue of currentGenerator.shrink(currentValue)) { yield [...value.slice(0, index), shrunkValue, ...value.slice(index + 1)]; } } } } } miniFc.array = (itemGenerator) => { return { generate(mrng) { const size = mrng.next(0, 10); const content = []; for (let index = 0 ; index !== size ; ++index) { content.push(itemGenerator.generate(mrng)); } return content; }, *shrink(value) { // No shrink on empty arrays if (value.length === 0) { return; } // Step 1. Shrink on size first by keeping last items let removedSize = Math.floor(value.length / 2); while (removedSize > 0) { yield value.slice(removedSize); removedSize = Math.floor(removedSize / 2); } // Step 2. Shrink the first item alone for (const shrunkItemValue of itemGenerator.shrink(value[0])) { yield [shrunkItemValue, ...value.slice(1)]; } // Step 3. Keep first item untouched for (const shrunkValue of this.shrink(value.slice(1))) { yield [value[0], ...shrunkValue]; } } } } miniFc.boolean = () => map( miniFc.integer(0, 1), Boolean, b => b ? 1 : 0, ) miniFc.character = () => map( miniFc.integer(0, 25), n => String.fromCharCode(97 + n), c => c.codePointAt(0) - 97, ) miniFc.string = () => map( miniFc.array(miniFc.character()), characters => characters.join(''), s => s.split('') ) miniFc.dictionary = (valueGenerator) => map( miniFc.array(miniFc.tuple(miniFc.string(), valueGenerator)), Object.fromEntries, Object.entries, ) miniFc.property = (generator, predicate) => { return { generate(mrng) { return generator.generate(mrng); }, shrink(value) { return generator.shrink(value); }, run(valueUnderTest) { return predicate(valueUnderTest); } } } function executeAndShrink(valueUnderTest, property) { if (!property.run(valueUnderTest)) { for (const shrunkValue of property.shrink(valueUnderTest)) { const shrunkResults = executeAndShrink(shrunkValue, property); if (shrunkResults.failed) { return shrunkResults; } } return { failed: true, value: valueUnderTest }; } return { failed: false }; } miniFc.assert = (property, { seed = Date.now() } = {}) => { let rng = prand.xoroshiro128plus(seed); for (let runId = 0 ; runId !== 100 ; ++runId) { const valueUnderTest = property.generate(new Random(rng)); const testResults = executeAndShrink(valueUnderTest, property); if (testResults.failed) { throw new Error(`Property failed after ${runId + 1} runs with value ${JSON.stringify(testResults.value)} (seed: ${seed})`); } rng = rng.jump(); } }
Let's use it!
const isSubstring = (pattern, text) => { return text.indexOf(pattern) > 0; } miniFc.assert( miniFc.property( miniFc.tuple(miniFc.string(), miniFc.string(), miniFc.string()), ([a, b, c]) => isSubstring(b, a + b + c) ) )
Loading…

no comments

    sign in to comment