test: add unit tests for args, dist, and stdin modules

This commit is contained in:
2026-06-12 15:38:59 +08:00
parent 2a17ca30cb
commit a9d35d03f2
3 changed files with 437 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
import { describe, test, expect } from "bun:test";
import { generate } from "../dist";
import { type Options, type Dist, defaultOptions } from "../types";
function opts(overrides: Partial<Options> & { 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 0100");
});
test("throws on decimals > 100", () => {
const o = opts({ dist: "uniform", decimals: 101 });
expect(() => [...generate(o)]).toThrow("decimals must be 0100");
});
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");
});
});