import { describe, test, expect } from "bun:test"; import { generate } from "../dist"; import { type Options, type Dist, defaultOptions } from "../types"; function opts(overrides: Partial & { dist: Dist }): Options { return { ...defaultOptions(overrides.dist), ...overrides }; } describe("generate", () => { test("uniform produces values in [min, max] for decimals=0 (inclusive)", () => { const n = 2000; const o = opts({ dist: "uniform", min: 10, max: 20, count: n, decimals: 0 }); const values = [...generate(o)]; expect(values.length).toBe(n); for (const v of values) { expect(v).toBeGreaterThanOrEqual(10); expect(v).toBeLessThanOrEqual(20); expect(Number.isInteger(v)).toBe(true); } // All values in [10,20] must appear at least once with high probability const seen = new Set(values); for (let i = 10; i <= 20; i++) expect(seen.has(i)).toBe(true); }); test("uniform produces values in [min, max) for decimals>0", () => { const o = opts({ dist: "uniform", min: 0, max: 1, count: 500, decimals: 4 }); const values = [...generate(o)]; for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThan(1); } }); test("uniform negative range produces inclusive values", () => { const o = opts({ dist: "uniform", min: -10, max: -5, count: 100, decimals: 0 }); const values = [...generate(o)]; for (const v of values) { expect(v).toBeGreaterThanOrEqual(-10); expect(v).toBeLessThanOrEqual(-5); expect(Number.isInteger(v)).toBe(true); } }); test("uniform single-value range", () => { const o = opts({ dist: "uniform", min: 7, max: 7, count: 50, decimals: 0 }); const values = [...generate(o)]; for (const v of values) expect(v).toBe(7); }); test("normal produces values around mean", () => { const o = opts({ dist: "normal", mean: 100, stddev: 15, count: 2000 }); const values = [...generate(o)]; const avg = values.reduce((a, b) => a + b, 0) / values.length; // Mean should be within ~3 std errors of 100 expect(avg).toBeGreaterThan(98); expect(avg).toBeLessThan(102); }); test("normal with decimals=0 produces integers via Math.round", () => { const o = opts({ dist: "normal", mean: 0, stddev: 1, count: 500, decimals: 0 }); const values = [...generate(o)]; for (const v of values) { expect(Number.isInteger(v)).toBe(true); } }); test("binomial produces values in [0, n]", () => { const o = opts({ dist: "binomial", trials: 10, prob: 0.5, count: 500 }); const values = [...generate(o)]; for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThanOrEqual(10); expect(Number.isInteger(v)).toBe(true); } }); test("binomial p=0 always returns 0", () => { const o = opts({ dist: "binomial", trials: 10, prob: 0, count: 50 }); for (const v of generate(o)) expect(v).toBe(0); }); test("binomial p=1 always returns n", () => { const o = opts({ dist: "binomial", trials: 10, prob: 1, count: 50 }); for (const v of generate(o)) expect(v).toBe(10); }); test("binomial trials=0 always returns 0", () => { const o = opts({ dist: "binomial", trials: 0, prob: 0.5, count: 50 }); for (const v of generate(o)) expect(v).toBe(0); }); test("binomial large n uses Normal approximation", () => { const o = opts({ dist: "binomial", trials: 20000, prob: 0.5, count: 200 }); const values = [...generate(o)]; const avg = values.reduce((a, b) => a + b, 0) / values.length; expect(avg).toBeGreaterThan(9500); expect(avg).toBeLessThan(10500); for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThanOrEqual(20000); } }); test("poisson produces non-negative integers", () => { const o = opts({ dist: "poisson", lambda: 5, count: 500 }); const values = [...generate(o)]; for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(Number.isInteger(v)).toBe(true); } }); test("poisson large lambda uses Normal approximation", () => { const o = opts({ dist: "poisson", lambda: 20000, count: 200 }); const values = [...generate(o)]; const avg = values.reduce((a, b) => a + b, 0) / values.length; expect(avg).toBeGreaterThan(19000); expect(avg).toBeLessThan(21000); for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(Number.isInteger(v)).toBe(true); } }); test("exponential produces non-negative values", () => { // decimals=0: Math.round can make very small values become 0 const o = opts({ dist: "exponential", lambda: 1, count: 500 }); for (const v of generate(o)) { expect(v).toBeGreaterThanOrEqual(0); } }); test("exponential mean approximates 1/lambda", () => { const lambda = 2; const o = opts({ dist: "exponential", lambda, count: 2000 }); const values = [...generate(o)]; const avg = values.reduce((a, b) => a + b, 0) / values.length; expect(avg).toBeGreaterThan(0.35); expect(avg).toBeLessThan(0.65); }); test("hypergeometric produces values in [0, min(K, n)]", () => { const o = opts({ dist: "hypergeometric", popSize: 100, successes: 30, draws: 10, count: 500 }); const values = [...generate(o)]; for (const v of values) { expect(v).toBeGreaterThanOrEqual(0); expect(v).toBeLessThanOrEqual(10); expect(Number.isInteger(v)).toBe(true); } }); test("hypergeometric K=0 always returns 0", () => { const o = opts({ dist: "hypergeometric", popSize: 100, successes: 0, draws: 10, count: 50 }); for (const v of generate(o)) expect(v).toBe(0); }); test("hypergeometric draws=0 always returns 0", () => { const o = opts({ dist: "hypergeometric", popSize: 100, successes: 50, draws: 0, count: 50 }); for (const v of generate(o)) expect(v).toBe(0); }); test("generator yields exactly count values", () => { for (const count of [1, 5, 100]) { const o = opts({ dist: "uniform", count }); expect([...generate(o)].length).toBe(count); } }); // validateOptions defense-in-depth tests test("throws on count < 1", () => { const o = opts({ dist: "uniform", count: 0 }); expect(() => [...generate(o)]).toThrow("invalid count"); }); test("throws on negative decimals", () => { const o = opts({ dist: "uniform", decimals: -1 }); expect(() => [...generate(o)]).toThrow("decimals must be 0–100"); }); test("throws on decimals > 100", () => { const o = opts({ dist: "uniform", decimals: 101 }); expect(() => [...generate(o)]).toThrow("decimals must be 0–100"); }); test("throws on min > max", () => { const o = opts({ dist: "uniform", min: 10, max: 5 }); expect(() => [...generate(o)]).toThrow("min"); }); test("throws on stddev <= 0", () => { const o = opts({ dist: "normal", stddev: 0 }); expect(() => [...generate(o)]).toThrow("stddev"); }); test("throws on prob out of range", () => { const o = opts({ dist: "binomial", prob: 1.5 }); expect(() => [...generate(o)]).toThrow("prob"); }); test("throws on lambda <= 0 for poisson", () => { const o = opts({ dist: "poisson", lambda: 0 }); expect(() => [...generate(o)]).toThrow("lambda"); }); test("throws on lambda <= 0 for exponential", () => { const o = opts({ dist: "exponential", lambda: -1 }); expect(() => [...generate(o)]).toThrow("lambda"); }); test("throws on hypergeometric K > N", () => { const o = opts({ dist: "hypergeometric", popSize: 100, successes: 150, draws: 10 }); expect(() => [...generate(o)]).toThrow("K"); }); });