Files
rand-tools/tests/random/core.test.ts
T
2026-09-01 08:27:00 +02:00

211 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
normalizeWordList,
passphrase,
passphraseWithWordList,
randomIdentifiers,
randomIntegers,
randomString,
rollDice,
ulid,
uuidV4,
uuidV7,
} from "../../src/random/generators";
import {
coinFlips,
dealCards,
decimalFractions,
integerSequence,
randomCoordinates,
randomDates,
} from "../../src/random/draws";
import { randomSource } from "../../src/random/source";
describe("random generators", () => {
it("repeats deterministic recipes", () =>
expect(
randomIntegers(randomSource("deterministic", "seed"), 10, 1, 6),
).toEqual(randomIntegers(randomSource("deterministic", "seed"), 10, 1, 6)));
it("produces RFC-shaped UUIDs", () => {
const source = randomSource("deterministic", "id");
expect(uuidV4(source)).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
);
expect(uuidV7(source, 0)).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
);
});
it("creates a 26-character ULID", () =>
expect(ulid(randomSource("deterministic", "id"), 0)).toMatch(
/^[0-9A-HJKMNP-TV-Z]{26}$/u,
));
it("bounds identifier batches before allocating their output array", () => {
const source = randomSource("deterministic", "identifier-bounds");
expect(() => randomIdentifiers(source, 0, "uuid4", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(() => randomIdentifiers(source, 100_001, "uuid7", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(() => randomIdentifiers(source, Number.NaN, "ulid", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(randomIdentifiers(source, 2, "uuid4", 0)).toHaveLength(2);
});
it("rejects an inclusive MAX_SAFE_INTEGER bound before adding one", () => {
const source = randomSource("deterministic", "integer-boundary");
expect(() =>
randomIntegers(
source,
1,
Number.MAX_SAFE_INTEGER,
Number.MAX_SAFE_INTEGER,
),
).toThrow(/less than Number\.MAX_SAFE_INTEGER/u);
expect(
randomIntegers(
source,
1,
Number.MAX_SAFE_INTEGER - 1,
Number.MAX_SAFE_INTEGER - 1,
),
).toEqual([Number.MAX_SAFE_INTEGER - 1]);
});
it("rejects biased duplicate alphabets", () =>
expect(() =>
randomString(randomSource("deterministic", "x"), 3, "aab"),
).toThrow(/duplicate/u));
it("bounds dice and reports the modifier", () =>
expect(
rollDice(randomSource("deterministic", "dice"), "4d6+2"),
).toMatchObject({ expression: "4d6+2", modifier: 2 }));
it("reports only the uniform-list entropy model", () =>
expect(
passphrase(randomSource("deterministic", "words"), 4, [
"one",
"two",
"three",
"four",
]).entropy,
).toBe(8));
it("normalizes a custom word list once and reports its eligible count", () => {
const words = normalizeWordList(" one \r\n\r\ntwo\n three ");
expect(words).toMatchObject({
words: ["one", "two", "three"],
canonical: "one\ntwo\nthree",
normalization: "trim-lines-drop-empty-preserve-order-v1",
});
expect(
passphraseWithWordList(
randomSource("deterministic", "normalized-words"),
4,
words,
),
).toMatchObject({ listSize: 3, entropy: 4 * Math.log2(3) });
});
it("bounds alphabet and word-list allocation before generation", () => {
const source = randomSource("deterministic", "bounds");
expect(() => randomString(source, 1, "ab".repeat(70_000))).toThrow(
/131,072/u,
);
expect(() => passphrase(source, 1, ["x".repeat(10_001), "y"])).toThrow(
/10,000/u,
);
});
});
describe("local draws", () => {
it("reproduces coin flips and reports only binary outcomes", () => {
const first = coinFlips(randomSource("deterministic", "coins"), 50);
const second = coinFlips(randomSource("deterministic", "coins"), 50);
expect(first).toEqual(second);
expect(first.every((value) => value === "Heads" || value === "Tails")).toBe(
true,
);
});
it("deals every card from a standard deck without replacement", () => {
const cards = dealCards(randomSource("deterministic", "cards"), 1, 52);
expect(cards).toHaveLength(52);
expect(new Set(cards).size).toBe(52);
expect(
cards.every((card) => /^(?:A|[2-9]|10|J|Q|K)[♠♥♦♣]$/u.test(card)),
).toBe(true);
});
it("shuffles an inclusive integer range exactly once", () => {
const values = integerSequence(
randomSource("deterministic", "sequence"),
-2,
3,
);
expect(values).toHaveLength(6);
expect([...values].sort((a, b) => a - b)).toEqual([-2, -1, 0, 1, 2, 3]);
});
it("filters dates by weekday and can sample without replacement", () => {
const values = randomDates(
randomSource("deterministic", "dates"),
4,
"2024-02-01",
"2024-02-29",
{ weekdays: [1], withoutReplacement: true },
);
expect(values).toHaveLength(4);
expect(new Set(values).size).toBe(4);
expect(
values.every((value) => new Date(`${value}T00:00:00Z`).getUTCDay() === 1),
).toBe(true);
expect(() =>
randomDates(
randomSource("deterministic", "bad-date"),
1,
"2023-02-29",
"2023-03-01",
),
).toThrow(/valid Gregorian/u);
});
it("generates exact-length decimal fractions with no binary formatting", () => {
const values = decimalFractions(
randomSource("deterministic", "fractions"),
4,
12,
);
expect(values).toHaveLength(4);
expect(values.every((value) => /^0\.\d{12}$/u.test(value))).toBe(true);
});
it("generates reproducible coordinates inside geographic bounds", () => {
const first = randomCoordinates(
randomSource("deterministic", "coordinates"),
100,
6,
);
const second = randomCoordinates(
randomSource("deterministic", "coordinates"),
100,
6,
);
expect(first).toEqual(second);
expect(
first.every(
({ latitude, longitude }) =>
latitude >= -90 &&
latitude <= 90 &&
longitude >= -180 &&
longitude <= 180,
),
).toBe(true);
});
it("rejects draw requests beyond their allocation bounds", () => {
const source = randomSource("deterministic", "bounds");
expect(() => coinFlips(source, 100_001)).toThrow(/100,000/u);
expect(() => dealCards(source, 1, 53)).toThrow(/152/u);
expect(() => integerSequence(source, 0, 100_000)).toThrow(/100,000/u);
expect(() => decimalFractions(source, 100_000, 64)).toThrow(/4,000,000/u);
expect(() => randomCoordinates(source, 1, 11)).toThrow(/010/u);
});
});