Skip to content

Commit 8b280a7

Browse files
authored
Merge pull request #190 from dahlia/failure-log-reporter
Scoped failure log reporting for tests
2 parents 6c91328 + 84bae9e commit 8b280a7

13 files changed

Lines changed: 1360 additions & 654 deletions

File tree

CHANGES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ To be released.
2323
[#185]: https://github.com/dahlia/logtape/issues/185
2424
[#188]: https://github.com/dahlia/logtape/pull/188
2525

26+
### @logtape/testing
27+
28+
- Added `createFailureLogReporter()` for buffering LogTape records during a
29+
test callback and reporting them only when the callback fails by default.
30+
The reporter uses scoped configuration, preserves wrapped callback
31+
parameters, and supports `mode: "on-failure"`, `mode: "always"`, and
32+
`mode: "never"`. [[#186], [#189]]
33+
34+
- Added `FailureLogReporter` interface with `wrap()` and `run()`
35+
methods.
36+
- Added `FailureLogReporterOptions` interface with `mode`,
37+
`lowestLevel`, `sink`, and `formatter` options.
38+
- Added `FailureLogReportMode` type.
39+
- Added `@logtape/testing/recorder` and
40+
`@logtape/testing/reporter` subpath exports while keeping the root
41+
`@logtape/testing` export for compatibility.
42+
43+
[#186]: https://github.com/dahlia/logtape/issues/186
44+
[#189]: https://github.com/dahlia/logtape/pull/189
45+
2646
### @logtape/file
2747

2848
- Added `TimeRotatingFileSinkOptions.parseFilename` option to customize how

docs/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ export default defineConfig({
296296
twoslashOptions: {
297297
compilerOptions: {
298298
lib: ["dom", "dom.iterable", "esnext"],
299+
module: 99, // ts.ModuleKind.ESNext
300+
moduleResolution: 100, // ts.ModuleResolutionKind.Bundler
299301
types: [
300302
"dom",
301303
"dom.iterable",

docs/manual/library.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ bun add @logtape/testing
129129
~~~~ typescript twoslash
130130
// @noErrors: 2307
131131
import { configure, reset } from "@logtape/logtape";
132-
import { createLogRecorder } from "@logtape/testing";
132+
import { createLogRecorder } from "@logtape/testing/recorder";
133133
import { Database } from "my-awesome-lib";
134134

135135
const recorder = createLogRecorder();

docs/manual/testing.md

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,13 @@ bun add @logtape/testing
9191

9292
~~~~ typescript twoslash
9393
// @noErrors: 2307
94+
import { after, before, test } from "node:test";
9495
import { configure, getLogger, reset } from "@logtape/logtape";
95-
import { createLogRecorder } from "@logtape/testing";
96+
import { createLogRecorder } from "@logtape/testing/recorder";
9697

9798
const recorder = createLogRecorder();
9899

99-
try {
100+
before(async () => {
100101
await configure({
101102
sinks: {
102103
recorder: recorder.sink, // [!code highlight]
@@ -110,7 +111,11 @@ try {
110111
{ category: ["logtape", "meta"], sinks: [] },
111112
],
112113
});
114+
});
115+
116+
after(reset);
113117

118+
test("case", () => {
114119
getLogger(["my-lib"]).info("User {userId} logged in.", {
115120
userId: 123,
116121
});
@@ -121,9 +126,7 @@ try {
121126
message: "User 123 logged in.",
122127
properties: { userId: 123 },
123128
});
124-
} finally {
125-
await reset();
126-
}
129+
});
127130
~~~~
128131

129132
The recorder stores records in sink call order. It snapshots lazy callback
@@ -145,6 +148,81 @@ sinks, still call `await dispose()` or `await reset()` as usual.
145148
[*@logtape/testing*]: https://jsr.io/@logtape/testing
146149

147150

151+
Failure log reporter
152+
--------------------
153+
154+
*This API is available since LogTape 2.3.0.*
155+
156+
When logs are useful only after a test fails, use
157+
`createFailureLogReporter()` from the [*@logtape/testing*] package. It
158+
buffers records while the wrapped callback runs, discards them when the
159+
callback succeeds, and reports them to a sink when the callback throws or
160+
rejects:
161+
162+
~~~~ typescript twoslash
163+
// @noErrors: 2307
164+
import { AsyncLocalStorage } from "node:async_hooks";
165+
import { after, before, test } from "node:test";
166+
import { configure, getLogger, reset } from "@logtape/logtape";
167+
import { createFailureLogReporter } from "@logtape/testing/reporter";
168+
169+
const reporter = createFailureLogReporter({
170+
lowestLevel: "debug",
171+
});
172+
173+
before(async () => {
174+
await configure({
175+
contextLocalStorage: new AsyncLocalStorage(),
176+
sinks: {},
177+
loggers: [
178+
{ category: ["logtape", "meta"], sinks: [] },
179+
],
180+
});
181+
});
182+
183+
after(reset);
184+
185+
test("case", reporter.wrap(async () => {
186+
getLogger(["my-lib"]).debug("Fixture state: {state}", {
187+
state: "ready",
188+
});
189+
190+
// Run assertions. The debug log is printed only if this callback fails.
191+
}));
192+
~~~~
193+
194+
The reporter uses scoped configuration, so it does not call `configure()` or
195+
`reset()` for each wrapped callback and does not mutate process-wide logger
196+
routing while a test is running. The process-wide configuration still must
197+
provide `~Config.contextLocalStorage`, because scoped configuration needs it to
198+
isolate the callback's logging policy.
199+
200+
Use `~FailureLogReporter.wrap()` when passing a callback to a test runner. It
201+
preserves callback parameters such as a test context or fixtures, and it always
202+
returns an async callback. Use `~FailureLogReporter.run()` when you want to
203+
invoke the callback directly:
204+
205+
~~~~ typescript twoslash
206+
// @noErrors: 2307
207+
import { createFailureLogReporter } from "@logtape/testing/reporter";
208+
209+
const reporter = createFailureLogReporter({
210+
lowestLevel: "debug",
211+
mode: "on-failure",
212+
});
213+
214+
await reporter.run(async () => {
215+
// Logs emitted here are reported only if this callback fails.
216+
});
217+
~~~~
218+
219+
Set `mode: "always"` to report buffered records even when the callback passes,
220+
or `mode: "never"` to suppress reporting while keeping the shared wrapper in
221+
place. By default the reporter writes formatted records to the console; pass
222+
`sink` to report records elsewhere, or `formatter` to customize the default
223+
console output.
224+
225+
148226
Buffer sink
149227
-----------
150228

packages/testing/README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Testing utilities for LogTape
99
This package provides testing utilities for [LogTape]. It includes a log
1010
recorder that collects `LogRecord` values in memory and provides matcher-based
1111
assertions for category, level, rendered message, raw message, and structured
12-
properties.
12+
properties. It also includes a failure log reporter that buffers records while
13+
a test callback runs and reports them only when the callback fails.
1314

1415
[JSR badge]: https://jsr.io/badges/@logtape/testing
1516
[JSR]: https://jsr.io/@logtape/testing
@@ -40,7 +41,7 @@ Use `createLogRecorder()` as a sink in tests:
4041

4142
~~~~ typescript
4243
import { configure, getLogger, reset } from "@logtape/logtape";
43-
import { createLogRecorder } from "@logtape/testing";
44+
import { createLogRecorder } from "@logtape/testing/recorder";
4445

4546
const recorder = createLogRecorder();
4647

@@ -77,6 +78,24 @@ that need lower-level access. Most property values are compared with
7778
matcher values match string property values. Rendered message matching uses
7879
the same value rendering as LogTape's default text formatter.
7980

81+
Use `createFailureLogReporter()` when logs are useful only after a test fails:
82+
83+
~~~~ typescript
84+
import { createFailureLogReporter } from "@logtape/testing/reporter";
85+
86+
const reporter = createFailureLogReporter({
87+
lowestLevel: "debug",
88+
});
89+
90+
test("case", reporter.wrap(async () => {
91+
// Logs emitted here are reported only if this callback throws.
92+
}));
93+
~~~~
94+
95+
The root `@logtape/testing` entry point re-exports both utilities for
96+
compatibility, but new code can import `@logtape/testing/recorder` or
97+
`@logtape/testing/reporter` to depend on only the relevant API surface.
98+
8099

81100
Docs
82101
----

packages/testing/deno.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
"name": "@logtape/testing",
33
"version": "2.3.0",
44
"license": "MIT",
5-
"exports": "./src/mod.ts",
5+
"exports": {
6+
".": "./src/mod.ts",
7+
"./recorder": "./src/recorder.ts",
8+
"./reporter": "./src/reporter.ts"
9+
},
610
"tasks": {
711
"build": "pnpm build",
812
"test": "deno test",

packages/testing/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@
4141
"import": "./dist/mod.js",
4242
"require": "./dist/mod.cjs"
4343
},
44+
"./recorder": {
45+
"types": {
46+
"import": "./dist/recorder.d.ts",
47+
"require": "./dist/recorder.d.cts"
48+
},
49+
"import": "./dist/recorder.js",
50+
"require": "./dist/recorder.cjs"
51+
},
52+
"./reporter": {
53+
"types": {
54+
"import": "./dist/reporter.d.ts",
55+
"require": "./dist/reporter.d.cts"
56+
},
57+
"import": "./dist/reporter.js",
58+
"require": "./dist/reporter.cjs"
59+
},
4460
"./package.json": "./package.json"
4561
},
4662
"sideEffects": false,

0 commit comments

Comments
 (0)