Files
rand-cli/src/__tests__/dist.test.ts
T

215 lines
7.5 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, 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");
});
});