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();
}
}