Skip to content

Commit 94d30a9

Browse files
committed
test: add comprehensive logger tests
Tests cover: - Log level filtering (debug, info, warn, error, off) - Child loggers with context prefixes - Platform DI (emitErrorAnnotation, startGroup, endGroup) - withGroup for sync/async functions with error handling - fatal() method with bug report URL - Argument formatting (Error, objects, null, undefined) - GitHub and GitLab platform implementations
1 parent 995f20d commit 94d30a9

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed

src/logger.test.ts

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import {
2+
afterEach,
3+
beforeEach,
4+
describe,
5+
expect,
6+
it,
7+
vi,
8+
type Mock,
9+
} from "vitest";
10+
import {
11+
createLogger,
12+
githubPlatform,
13+
gitlabPlatform,
14+
type Platform,
15+
} from "./logger";
16+
17+
describe("logger", () => {
18+
let stdoutSpy: Mock;
19+
let stderrSpy: Mock;
20+
21+
beforeEach(() => {
22+
stdoutSpy = vi
23+
.spyOn(process.stdout, "write")
24+
.mockImplementation(() => true) as unknown as Mock;
25+
stderrSpy = vi
26+
.spyOn(process.stderr, "write")
27+
.mockImplementation(() => true) as unknown as Mock;
28+
});
29+
30+
afterEach(() => {
31+
vi.restoreAllMocks();
32+
});
33+
34+
function mockPlatform(): Platform & {
35+
calls: { method: string; args: unknown[] }[];
36+
} {
37+
const calls: { method: string; args: unknown[] }[] = [];
38+
return {
39+
calls,
40+
emitErrorAnnotation(message: string) {
41+
calls.push({ method: "emitErrorAnnotation", args: [message] });
42+
},
43+
startGroup(name: string) {
44+
calls.push({ method: "startGroup", args: [name] });
45+
return `group-${calls.length}`;
46+
},
47+
endGroup(id: string) {
48+
calls.push({ method: "endGroup", args: [id] });
49+
},
50+
};
51+
}
52+
53+
describe("log levels", () => {
54+
it("filters messages below configured level", () => {
55+
const platform = mockPlatform();
56+
const logger = createLogger({ platform, level: "warn" });
57+
58+
logger.debug("debug message");
59+
logger.info("info message");
60+
logger.warn("warn message");
61+
logger.error("error message");
62+
63+
const output = stderrSpy.mock.calls.map((c) => c[0]).join("");
64+
expect(output).not.toContain("debug message");
65+
expect(output).not.toContain("info message");
66+
expect(output).toContain("warn message");
67+
expect(output).toContain("error message");
68+
});
69+
70+
it("logs all messages at debug level", () => {
71+
const platform = mockPlatform();
72+
const logger = createLogger({ platform, level: "debug" });
73+
74+
logger.debug("debug message");
75+
logger.info("info message");
76+
77+
const stdout = stdoutSpy.mock.calls.map((c) => c[0]).join("");
78+
expect(stdout).toContain("debug message");
79+
expect(stdout).toContain("info message");
80+
});
81+
82+
it("logs nothing at off level", () => {
83+
const platform = mockPlatform();
84+
const logger = createLogger({ platform, level: "off" });
85+
86+
logger.debug("debug");
87+
logger.info("info");
88+
logger.warn("warn");
89+
logger.error("error");
90+
91+
expect(stdoutSpy).not.toHaveBeenCalled();
92+
expect(stderrSpy).not.toHaveBeenCalled();
93+
});
94+
});
95+
96+
describe("child loggers", () => {
97+
it("adds context prefix to messages", () => {
98+
const platform = mockPlatform();
99+
const logger = createLogger({ platform, level: "info" });
100+
const child = logger.child("build");
101+
102+
child.info("starting");
103+
104+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
105+
expect(output).toContain("[build]");
106+
expect(output).toContain("starting");
107+
});
108+
109+
it("chains context prefixes", () => {
110+
const platform = mockPlatform();
111+
const logger = createLogger({ platform, level: "info" });
112+
const child = logger.child("build").child("typescript");
113+
114+
child.info("compiling");
115+
116+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
117+
expect(output).toContain("[build:typescript]");
118+
});
119+
});
120+
121+
describe("platform integration", () => {
122+
it("calls emitErrorAnnotation on error", () => {
123+
const platform = mockPlatform();
124+
const logger = createLogger({ platform, level: "error" });
125+
126+
logger.error("something failed");
127+
128+
expect(platform.calls).toContainEqual({
129+
method: "emitErrorAnnotation",
130+
args: ["something failed"],
131+
});
132+
});
133+
134+
it("does not call emitErrorAnnotation on warn", () => {
135+
const platform = mockPlatform();
136+
const logger = createLogger({ platform, level: "warn" });
137+
138+
logger.warn("something warned");
139+
140+
expect(platform.calls).not.toContainEqual(
141+
expect.objectContaining({ method: "emitErrorAnnotation" }),
142+
);
143+
});
144+
145+
it("calls startGroup and endGroup", () => {
146+
const platform = mockPlatform();
147+
const logger = createLogger({ platform, level: "info" });
148+
149+
logger.group("Test Group");
150+
logger.groupEnd();
151+
152+
expect(platform.calls).toEqual([
153+
{ method: "startGroup", args: ["Test Group"] },
154+
{ method: "endGroup", args: ["group-1"] },
155+
]);
156+
});
157+
});
158+
159+
describe("withGroup", () => {
160+
it("wraps sync function in group", () => {
161+
const platform = mockPlatform();
162+
const logger = createLogger({ platform, level: "info" });
163+
164+
const result = logger.withGroup("Sync Work", () => {
165+
return 42;
166+
});
167+
168+
expect(result).toBe(42);
169+
expect(platform.calls).toEqual([
170+
{ method: "startGroup", args: ["Sync Work"] },
171+
{ method: "endGroup", args: ["group-1"] },
172+
]);
173+
});
174+
175+
it("wraps async function in group", async () => {
176+
const platform = mockPlatform();
177+
const logger = createLogger({ platform, level: "info" });
178+
179+
const result = await logger.withGroup("Async Work", async () => {
180+
return "done";
181+
});
182+
183+
expect(result).toBe("done");
184+
expect(platform.calls).toEqual([
185+
{ method: "startGroup", args: ["Async Work"] },
186+
{ method: "endGroup", args: ["group-1"] },
187+
]);
188+
});
189+
190+
it("ends group on sync error", () => {
191+
const platform = mockPlatform();
192+
const logger = createLogger({ platform, level: "info" });
193+
194+
expect(() =>
195+
logger.withGroup("Failing Work", () => {
196+
throw new Error("oops");
197+
}),
198+
).toThrow("oops");
199+
200+
expect(platform.calls).toEqual([
201+
{ method: "startGroup", args: ["Failing Work"] },
202+
{ method: "endGroup", args: ["group-1"] },
203+
]);
204+
});
205+
206+
it("ends group on async error", async () => {
207+
const platform = mockPlatform();
208+
const logger = createLogger({ platform, level: "info" });
209+
210+
await expect(
211+
logger.withGroup("Failing Async", async () => {
212+
throw new Error("async oops");
213+
}),
214+
).rejects.toThrow("async oops");
215+
216+
expect(platform.calls).toEqual([
217+
{ method: "startGroup", args: ["Failing Async"] },
218+
{ method: "endGroup", args: ["group-1"] },
219+
]);
220+
});
221+
});
222+
223+
describe("fatal", () => {
224+
it("logs error and bug report URL", () => {
225+
const platform = mockPlatform();
226+
const logger = createLogger({ platform, level: "error" });
227+
228+
logger.fatal("Something broke", new Error("test error"));
229+
230+
const output = stderrSpy.mock.calls.map((c) => c[0]).join("");
231+
expect(output).toContain("Something broke");
232+
expect(output).toContain("This is a bug");
233+
expect(output).toContain(
234+
"https://github.com/stainless-api/upload-openapi-spec-action/issues",
235+
);
236+
});
237+
238+
it("calls platform error annotation", () => {
239+
const platform = mockPlatform();
240+
const logger = createLogger({ platform, level: "error" });
241+
242+
logger.fatal("Fatal error occurred");
243+
244+
expect(platform.calls).toContainEqual({
245+
method: "emitErrorAnnotation",
246+
args: ["Fatal error occurred"],
247+
});
248+
});
249+
});
250+
251+
describe("argument formatting", () => {
252+
it("formats Error objects with stack trace", () => {
253+
const platform = mockPlatform();
254+
const logger = createLogger({ platform, level: "error" });
255+
const error = new Error("test error");
256+
257+
logger.error("Failed:", error);
258+
259+
const output = stderrSpy.mock.calls.map((c) => c[0]).join("");
260+
expect(output).toContain("test error");
261+
expect(output).toContain("Error:");
262+
});
263+
264+
it("formats objects as JSON", () => {
265+
const platform = mockPlatform();
266+
const logger = createLogger({ platform, level: "info" });
267+
268+
logger.info("Data:", { foo: "bar", count: 42 });
269+
270+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
271+
expect(output).toContain('"foo": "bar"');
272+
expect(output).toContain('"count": 42');
273+
});
274+
275+
it("handles null and undefined", () => {
276+
const platform = mockPlatform();
277+
const logger = createLogger({ platform, level: "info" });
278+
279+
logger.info("Values:", null, undefined);
280+
281+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
282+
expect(output).toContain("null");
283+
expect(output).toContain("undefined");
284+
});
285+
});
286+
287+
describe("platform implementations", () => {
288+
describe("githubPlatform", () => {
289+
it("emits ::error:: annotation", () => {
290+
githubPlatform.emitErrorAnnotation?.("test error");
291+
292+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
293+
expect(output).toBe("::error::test error\n");
294+
});
295+
296+
it("emits ::group:: and ::endgroup::", () => {
297+
const id = githubPlatform.startGroup("Test");
298+
githubPlatform.endGroup(id);
299+
300+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
301+
expect(output).toContain("::group::Test\n");
302+
expect(output).toContain("::endgroup::\n");
303+
});
304+
});
305+
306+
describe("gitlabPlatform", () => {
307+
it("does not have emitErrorAnnotation", () => {
308+
expect(gitlabPlatform.emitErrorAnnotation).toBeUndefined();
309+
});
310+
311+
it("emits section_start and section_end", () => {
312+
const id = gitlabPlatform.startGroup("Test Section");
313+
gitlabPlatform.endGroup(id);
314+
315+
const output = stdoutSpy.mock.calls.map((c) => c[0]).join("");
316+
expect(output).toMatch(/section_start:\d+:/);
317+
expect(output).toContain("Test Section");
318+
expect(output).toMatch(/section_end:\d+:/);
319+
});
320+
});
321+
});
322+
});

0 commit comments

Comments
 (0)