128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
PROFILER_CODES,
|
|
type ProfilerError,
|
|
} from "../error-codes/error-codes.js";
|
|
import type { ProfileEntry, ProfileMark } from "../types/profiler.types.js";
|
|
import { ProfileReporter } from "./reporter.js";
|
|
|
|
function entry(
|
|
label: string,
|
|
duration: number,
|
|
category?: string
|
|
): ProfileEntry {
|
|
return { label, startTime: 0, endTime: duration, duration, category };
|
|
}
|
|
|
|
describe("ProfileReporter", () => {
|
|
it("throws INVALID_OPTIONS for non-empty bad percentiles", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
new ProfileReporter({ percentiles: [200] });
|
|
throw new Error("expected throw");
|
|
} catch (err) {
|
|
expect((err as ProfilerError).code).toBe(
|
|
PROFILER_CODES.INVALID_OPTIONS
|
|
);
|
|
}
|
|
});
|
|
|
|
it("throws INVALID_OPTIONS for empty percentiles", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
new ProfileReporter({ percentiles: [] });
|
|
throw new Error("expected throw");
|
|
} catch (err) {
|
|
expect((err as ProfilerError).code).toBe(
|
|
PROFILER_CODES.INVALID_OPTIONS
|
|
);
|
|
}
|
|
});
|
|
|
|
it("computes percentiles", { timeout: 5000 }, () => {
|
|
const r = new ProfileReporter({
|
|
percentiles: [50, 90],
|
|
clock: () => 0,
|
|
});
|
|
const stats = r.computeStats([10, 20, 30, 40, 50]);
|
|
expect(stats.count).toBe(5);
|
|
expect(stats.min).toBe(10);
|
|
expect(stats.max).toBe(50);
|
|
expect(stats.mean).toBe(30);
|
|
expect(stats.median).toBe(30);
|
|
expect(stats.percentiles[50]).toBe(30);
|
|
expect(stats.percentiles[90]).toBeGreaterThan(30);
|
|
});
|
|
|
|
it("handles empty entries", { timeout: 5000 }, () => {
|
|
const r = new ProfileReporter();
|
|
const stats = r.computeStats([]);
|
|
expect(stats).toEqual({
|
|
count: 0,
|
|
min: 0,
|
|
max: 0,
|
|
mean: 0,
|
|
median: 0,
|
|
percentiles: {},
|
|
totalDuration: 0,
|
|
});
|
|
});
|
|
|
|
it("groups by category and uncategorized falls back", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const r = new ProfileReporter({ clock: () => 0 });
|
|
const report = r.generateReport([
|
|
entry("a", 10, "render"),
|
|
entry("b", 20, "render"),
|
|
entry("c", 30),
|
|
]);
|
|
expect(report.byCategory.render?.count).toBe(2);
|
|
expect(report.byCategory.uncategorized?.count).toBe(1);
|
|
});
|
|
|
|
it("flags alerts above threshold", { timeout: 5000 }, () => {
|
|
const r = new ProfileReporter({ alertThreshold: 20, clock: () => 0 });
|
|
const report = r.generateReport([entry("ok", 10), entry("bad", 30)]);
|
|
expect(report.alerts).toHaveLength(1);
|
|
expect(report.alerts[0]?.label).toBe("bad");
|
|
});
|
|
|
|
it("formatReport produces stable text", { timeout: 5000 }, () => {
|
|
const r = new ProfileReporter({ clock: () => 0, alertThreshold: 1 });
|
|
const report = r.generateReport([entry("slow", 10)]);
|
|
const text = r.formatReport(report);
|
|
expect(text).toContain("Profile Report");
|
|
expect(text).toContain("Alerts (1)");
|
|
expect(text).toContain("slow");
|
|
});
|
|
|
|
it("formatReport with no alerts skips alerts section", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const r = new ProfileReporter({ clock: () => 0 });
|
|
const text = r.formatReport(r.generateReport([entry("ok", 10)]));
|
|
expect(text).not.toContain("Alerts");
|
|
});
|
|
|
|
it("computePercentile handles single-element exact index", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const r = new ProfileReporter();
|
|
const stats = r.computeStats([42]);
|
|
expect(stats.median).toBe(42);
|
|
});
|
|
|
|
it("ignores entries without duration", { timeout: 5000 }, () => {
|
|
const r = new ProfileReporter();
|
|
const report = r.generateReport(
|
|
[{ label: "still", startTime: 0 }, entry("ok", 10)],
|
|
[{ label: "mark", timestamp: 1 }] as readonly ProfileMark[]
|
|
);
|
|
expect(report.stats.count).toBe(1);
|
|
expect(report.marks).toHaveLength(1);
|
|
});
|
|
});
|