test: add unit tests for args, dist, and stdin modules
This commit is contained in:
@@ -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 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user