130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import * as fc from "fast-check";
|
|
import { decode } from "../src/decoder/decoder.js";
|
|
import { encodeDeterministic } from "../src/deterministic/deterministic.js";
|
|
import { encode } from "../src/encoder/encoder.js";
|
|
import { decodeSequence, encodeSequence } from "../src/sequences/sequences.js";
|
|
|
|
/**
|
|
* Recursive arbitrary covering the JSON-shaped subset of CBOR values that
|
|
* the encoder accepts and the decoder produces verbatim. Excludes:
|
|
* - `undefined` (excluded by deterministic-mode round-trip tests)
|
|
* - `Map` / `Tagged` / `bigint` (covered by targeted unit tests)
|
|
* - Float values that aren't exactly representable in float32 (the
|
|
* default codec selects float64, which round-trips, but our equality
|
|
* check uses `toEqual` so we exclude NaN).
|
|
*/
|
|
const cborValue = (): fc.Arbitrary<unknown> => {
|
|
const leaves: fc.Arbitrary<unknown> = fc.oneof(
|
|
fc.constant(null),
|
|
fc.boolean(),
|
|
fc.integer({ min: -1_000_000, max: 1_000_000 }),
|
|
fc.string(),
|
|
fc.uint8Array({ minLength: 0, maxLength: 32 })
|
|
);
|
|
return fc.letrec((tie) => ({
|
|
v: fc.oneof({ depthSize: "small" }, leaves, tie("array"), tie("obj")),
|
|
array: fc.array(tie("v"), { maxLength: 6 }),
|
|
obj: fc.dictionary(fc.string(), tie("v"), { maxKeys: 6 }),
|
|
})).v;
|
|
};
|
|
|
|
describe("encode + decode round-trip", () => {
|
|
it("default mode preserves arbitrary JSON-shaped values", {
|
|
timeout: 30000,
|
|
}, () => {
|
|
fc.assert(
|
|
fc.property(cborValue(), (value) => {
|
|
const wire = encode(value);
|
|
const back = decode(wire);
|
|
// CBOR maps decode as `Map`; plain objects in our arbitrary
|
|
// become a Map after decode. Compare via JSON-shaped projection.
|
|
expect(toComparable(back)).toEqual(toComparable(value));
|
|
}),
|
|
{ numRuns: 200 }
|
|
);
|
|
});
|
|
|
|
it("deterministic mode is byte-stable across two runs on the same input", {
|
|
timeout: 30000,
|
|
}, () => {
|
|
fc.assert(
|
|
fc.property(cborValue(), (value) => {
|
|
const a = encodeDeterministic(value);
|
|
const b = encodeDeterministic(value);
|
|
expect(Array.from(a)).toEqual(Array.from(b));
|
|
}),
|
|
{ numRuns: 200 }
|
|
);
|
|
});
|
|
|
|
it("deterministic mode produces byte-identical output for shuffled-key objects", {
|
|
timeout: 30000,
|
|
}, () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.dictionary(
|
|
fc.string({ minLength: 1, maxLength: 8 }),
|
|
fc.integer(),
|
|
{
|
|
minKeys: 1,
|
|
maxKeys: 8,
|
|
}
|
|
),
|
|
(obj) => {
|
|
const keys = Object.keys(obj);
|
|
if (keys.length < 2) return;
|
|
// Build a key-shuffled copy
|
|
const shuffled: Record<string, number> = {};
|
|
for (const k of [...keys].reverse())
|
|
shuffled[k] = obj[k] as number;
|
|
expect(Array.from(encodeDeterministic(obj))).toEqual(
|
|
Array.from(encodeDeterministic(shuffled))
|
|
);
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("CBOR Sequences round-trip", () => {
|
|
it("encode + decode preserves mixed-type item lists", {
|
|
timeout: 30000,
|
|
}, () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(cborValue(), { minLength: 0, maxLength: 8 }),
|
|
(items) => {
|
|
const wire = encodeSequence(items);
|
|
const back = decodeSequence(wire);
|
|
expect(back.map(toComparable)).toEqual(
|
|
items.map(toComparable)
|
|
);
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Project decoded values back into a comparable JSON-shaped form so
|
|
* `Map`-decoded objects equate to their plain-object inputs.
|
|
*/
|
|
function toComparable(v: unknown): unknown {
|
|
if (v === null || typeof v !== "object") return v;
|
|
if (v instanceof Uint8Array) return Array.from(v);
|
|
if (Array.isArray(v)) return v.map(toComparable);
|
|
if (v instanceof Map) {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, val] of v) out[String(k)] = toComparable(val);
|
|
return out;
|
|
}
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
out[k] = toComparable(val);
|
|
}
|
|
return out;
|
|
}
|