Repository navigation
test: print server log lines only for failing tests #788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
a211cf0
test: print server log lines only for failing tests
aliasunder d179417
test: pin the logger's console write shape that the log routing relie…
aliasunder 2283c25
style: clarify the log-line routing setup file and its config comment
aliasunder 5e18922
test: cover the log-line routing setup file's routing and pass-through
aliasunder f84455b
style: give the log-line routing factory an explicit return type
aliasunder 3d479a1
test: check the routing the setup file installs on the real streams
aliasunder File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Vitest setup file that keeps src/logger.ts's lines out of passing tests' output. | ||
| * - The logger writes each JSON line straight to process.stdout or | ||
| * process.stderr, which vitest prints for every test. | ||
| * - This wrapper sends those lines through console instead, so vitest files | ||
| * each line under the test that wrote it, and `silent: "passed-only"` prints | ||
| * the line only when that test fails. | ||
| * - A test that spies on process.stdout.write puts its spy in front of the | ||
| * wrapper, so the spy still sees every line it asserts on. | ||
| * - Under vitest's default isolation, each test file runs in its own process, | ||
| * so the wrappers last until that process exits and need no restore. | ||
| */ | ||
|
|
||
| /** Every line src/logger.ts writes opens with its timestamp field. */ | ||
| const LOG_LINE_START = '{"timestamp":' | ||
|
|
||
| type WriteCallback = (error?: Error | null) => void | ||
|
|
||
| type StreamWrite = ( | ||
| chunk: Uint8Array | string, | ||
| encodingOrCallback?: BufferEncoding | WriteCallback, | ||
| callback?: WriteCallback, | ||
| ) => boolean | ||
|
|
||
| export const createLogLineRoutingWrite = ( | ||
| stream: Pick<NodeJS.WriteStream, "write">, | ||
| logToConsole: (line: string) => void, | ||
| ): StreamWrite => { | ||
| const originalWrite = stream.write.bind(stream) | ||
|
|
||
| return (chunk, encodingOrCallback, callback) => { | ||
| // src/logger.ts passes neither an encoding nor a callback, so a write that | ||
| // carries one is never a log line. logger.test.ts pins this write shape. | ||
| const isLogLine = | ||
| typeof chunk === "string" && | ||
| chunk.startsWith(LOG_LINE_START) && | ||
| !encodingOrCallback && | ||
| !callback | ||
|
|
||
| if (isLogLine) { | ||
| // The logger's line ends in a newline, and console adds its own. | ||
| logToConsole(chunk.trimEnd()) | ||
| return true | ||
| } | ||
|
|
||
| // The callback sits second or third in write's overloads, so each position | ||
| // needs its own call to type-check. Node handles both calls the same way. | ||
| if (typeof encodingOrCallback === "function") return originalWrite(chunk, encodingOrCallback) | ||
| return originalWrite(chunk, encodingOrCallback, callback) | ||
| } | ||
|
aliasunder marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| // vitest captures console per test, unlike process.stdout, and its console never | ||
| // writes to process.stdout, so a routed line cannot loop back through the wrapper. | ||
| // eslint-disable-next-line no-console | ||
| process.stdout.write = createLogLineRoutingWrite(process.stdout, (line) => console.log(line)) | ||
| // eslint-disable-next-line no-console | ||
| process.stderr.write = createLogLineRoutingWrite(process.stderr, (line) => console.error(line)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { describe, expect, it, onTestFinished, vi } from "vitest" | ||
|
|
||
| import { createLogLineRoutingWrite } from "./helpers/route-log-lines-to-console.js" | ||
|
|
||
| type WriteCallback = (error?: Error | null) => void | ||
|
|
||
| /** A line in the shape src/logger.ts writes, trailing newline included. */ | ||
| const LOG_LINE = '{"timestamp":"2026-11-12T21:00:00.000-05:00","level":"info","message":"probe"}\n' | ||
|
|
||
| /** LOG_LINE as the console receives it. */ | ||
| const ROUTED_LOG_LINE = | ||
| '{"timestamp":"2026-11-12T21:00:00.000-05:00","level":"info","message":"probe"}' | ||
|
|
||
| const writeCallback: WriteCallback = () => undefined | ||
|
|
||
| const createRoutingWrite = () => { | ||
| // The stub returns false, which a real stream returns when the caller should | ||
| // wait, so a wrapper that returns true instead of the stream's result fails. | ||
| const streamWrite = vi.fn<(...writeArgs: unknown[]) => boolean>(() => false) | ||
| const logToConsole = vi.fn<(line: string) => void>() | ||
| const routingWrite = createLogLineRoutingWrite({ write: streamWrite }, logToConsole) | ||
|
|
||
| return { streamWrite, logToConsole, routingWrite } | ||
| } | ||
|
|
||
| describe("createLogLineRoutingWrite", () => { | ||
| it("sends a log line to the console without its trailing newline and never to the stream", () => { | ||
| const { streamWrite, logToConsole, routingWrite } = createRoutingWrite() | ||
|
|
||
| expect(routingWrite(LOG_LINE)).toBe(true) | ||
| expect(logToConsole).toHaveBeenCalledTimes(1) | ||
| expect(logToConsole).toHaveBeenCalledWith(ROUTED_LOG_LINE) | ||
| expect(streamWrite).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| type PassThroughCase = { | ||
| label: string | ||
| chunk: Uint8Array | string | ||
| encodingOrCallback?: BufferEncoding | WriteCallback | ||
| callback?: WriteCallback | ||
| expectedStreamArgs: ReadonlyArray<unknown> | ||
| } | ||
|
|
||
| const passThroughCases: ReadonlyArray<PassThroughCase> = [ | ||
| { | ||
| label: "text that does not open with a timestamp field", | ||
| chunk: "plain output\n", | ||
| expectedStreamArgs: ["plain output\n", undefined, undefined], | ||
| }, | ||
| { | ||
| label: "a log line written as bytes", | ||
| chunk: Buffer.from(LOG_LINE), | ||
| expectedStreamArgs: [Buffer.from(LOG_LINE), undefined, undefined], | ||
| }, | ||
| { | ||
| label: "a log line with an encoding", | ||
| chunk: LOG_LINE, | ||
| encodingOrCallback: "utf8", | ||
| expectedStreamArgs: [LOG_LINE, "utf8", undefined], | ||
| }, | ||
| { | ||
| label: "a log line with a callback in second place", | ||
| chunk: LOG_LINE, | ||
| encodingOrCallback: writeCallback, | ||
| expectedStreamArgs: [LOG_LINE, writeCallback], | ||
| }, | ||
| { | ||
| label: "a log line with an encoding and a callback", | ||
| chunk: LOG_LINE, | ||
| encodingOrCallback: "utf8", | ||
| callback: writeCallback, | ||
| expectedStreamArgs: [LOG_LINE, "utf8", writeCallback], | ||
| }, | ||
| ] | ||
|
|
||
| it.each(passThroughCases)( | ||
| "passes $label to the stream and returns the stream's result", | ||
| ({ chunk, encodingOrCallback, callback, expectedStreamArgs }) => { | ||
| const { streamWrite, logToConsole, routingWrite } = createRoutingWrite() | ||
|
|
||
| expect(routingWrite(chunk, encodingOrCallback, callback)).toBe(false) | ||
| expect(streamWrite).toHaveBeenCalledTimes(1) | ||
| expect(streamWrite).toHaveBeenCalledWith(...expectedStreamArgs) | ||
| expect(logToConsole).not.toHaveBeenCalled() | ||
| }, | ||
| ) | ||
| }) | ||
|
aliasunder marked this conversation as resolved.
Resolved
|
||
|
|
||
| describe("the routing the setup file installs", () => { | ||
| const installedRoutingCases = [ | ||
| { label: "process.stdout to console.log", stream: process.stdout, consoleMethod: "log" }, | ||
| { label: "process.stderr to console.error", stream: process.stderr, consoleMethod: "error" }, | ||
| ] as const | ||
|
|
||
| it.each(installedRoutingCases)( | ||
| "sends a log line written to $label", | ||
| ({ stream, consoleMethod }) => { | ||
| const consoleSpy = vi.spyOn(console, consoleMethod).mockImplementation(() => undefined) | ||
| onTestFinished(() => consoleSpy.mockRestore()) | ||
|
|
||
| stream.write(LOG_LINE) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledTimes(1) | ||
| expect(consoleSpy).toHaveBeenCalledWith(ROUTED_LOG_LINE) | ||
| }, | ||
| ) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.