Initial Commit
This commit is contained in:
129
packages/foundation/flow/cbor/tests/property.test.ts
Normal file
129
packages/foundation/flow/cbor/tests/property.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user