Skip to content

Commit a6aecd0

Browse files
committed
tests: improve fuzz tests
Can now generate values of arbitrary bit widths. We do this to make sure we cover more varied inputs more consistently, and restrict fuzzing for certain auras to inputs that make sense for that aura. (For example, no inputs >32 bits for `@if`, because any additional bits would get truncated, breaking the round-tripping that we test for.) Additionally, we make it possible to fuzz floats by letting you specify a "transform" function for the generated test values. This way, we can ensure none of our inputs are `NaN` values, which would have their mantissa ("payload") bits dropped during rendering.
1 parent 77c4a12 commit a6aecd0

File tree

1 file changed

+58
-18
lines changed

1 file changed

+58
-18
lines changed

test/fuzz.test.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import { tryParse as parse } from "../src/parse";
33
import render from '../src/render';
44
import { webcrypto } from 'crypto';
55

6-
const testCount = 500;
6+
// tests per bit-size
7+
const testCount = 50;
78

8-
const auras: aura[] = [
9+
const simpleAuras: aura[] = [
910
'c',
1011
'da',
1112
// 'dr', //TODO unsupported
1213
// 'f', // limited legitimate values
13-
// 'if', // fixed size
14-
// 'is', // fixed size
14+
// 'if', // won't round-trip above "native" size limit
15+
// 'is', // won't round-trip above "native" size limit
1516
// 'n', // limited legitimate values
1617
'p',
1718
'q',
18-
// 'rh', // strict size limits, and NaN won't always round-trip
19+
// 'rh', // strict size limits, and NaN usually won't round-trip
1920
// 'rd', //
2021
// 'rq', //
2122
// 'rs', //
@@ -37,21 +38,60 @@ const auras: aura[] = [
3738
'ux'
3839
]
3940

40-
function fuzz(nom: string, arr: Uint8Array | Uint16Array | Uint32Array | BigUint64Array) {
41-
webcrypto.getRandomValues(arr);
42-
auras.forEach((a) => {
43-
describe(nom + ' @' + a, () => {
44-
it('round-trips losslessly', () => {
45-
arr.forEach((n) => {
46-
n = BigInt(n);
47-
expect(parse(a, render(a, n))).toEqual(n);
48-
});
41+
function fuzz(minBits: number, maxBits: number, auras: aura[], f: (n:bigint)=>bigint = (n)=>n) {
42+
describe(minBits + '—' + maxBits + '-bit values', () => {
43+
const tests: { [bits: number]: bigint[] } = {};
44+
for (let bits = minBits; bits <= maxBits; bits++) {
45+
const parts = Math.ceil(bits / 64);
46+
const src = bits <= 8 ? new Uint8Array(testCount)
47+
: bits <= 16 ? new Uint16Array(testCount)
48+
: bits <= 32 ? new Uint32Array(testCount)
49+
: bits <= 64 ? new BigUint64Array(testCount)
50+
: new BigUint64Array(testCount * parts);
51+
webcrypto.getRandomValues(src);
52+
const arr: bigint[] = [];
53+
if (bits <= 64) {
54+
src.forEach((n) => arr.push(f(BigInt(n))));
55+
} else {
56+
const mask = (1n << BigInt(bits % 64)) - 1n;
57+
for (let t = 0; t < testCount; t+=parts) {
58+
let num = (src[t] as bigint) & mask;
59+
for (let p = 1; p < parts; p++) {
60+
num = (num << 64n) | (src[t+p] as bigint);
61+
}
62+
arr.push(f(num));
63+
}
64+
}
65+
tests[bits] = arr;
66+
}
67+
auras.forEach((a) => {
68+
it(a + ' round-trips losslessly', () => {
69+
for (let bits = minBits; bits <= maxBits; bits++) {
70+
tests[bits].forEach((n) => {
71+
n = BigInt(n);
72+
expect(parse(a, render(a, n))).toEqual(n);
73+
});
74+
}
4975
});
5076
});
5177
});
5278
}
5379

54-
fuzz('8-bit', new Uint8Array(testCount));
55-
fuzz('16-bit', new Uint16Array(testCount));
56-
fuzz('32-bit', new Uint32Array(testCount));
57-
fuzz('64-bit', new BigUint64Array(testCount));
80+
fuzz( 1, 32, [...simpleAuras, 'if', 'is']);
81+
fuzz(33, 64, [...simpleAuras, 'is']);
82+
fuzz(65, 128, simpleAuras);
83+
84+
// for floats, avoid NaNs, whose payload gets discarded during rendering,
85+
// and as such usually won't round-trip cleanly.
86+
// NaNs are encoded as "exponent bits all 1, non-zero significand",
87+
// so we hard-set one pseudo-random exponent bit to 0
88+
function safeFloat(size: bigint, w: bigint, p: bigint) {
89+
const full = (1n << size) - 1n;
90+
const makemask = (n: bigint) => full ^ (1n << (p + (n % w)));
91+
return (n: bigint) => n & makemask(n);
92+
}
93+
//TODO tests fail, off-by-one...
94+
// fuzz(4, 16, ['rh'], safeFloat( 16n, 5n, 10n));
95+
// fuzz(4, 32, ['rs'], safeFloat( 32n, 8n, 23n));
96+
// fuzz(4, 64, ['rd'], safeFloat( 64n, 11n, 52n));
97+
// fuzz(4, 128, ['rq'], safeFloat(128n, 15n, 112n));

0 commit comments

Comments
 (0)