@logtape/logtape 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/deno.json +0 -36
- package/src/config.test.ts +0 -589
- package/src/config.ts +0 -409
- package/src/context.test.ts +0 -187
- package/src/context.ts +0 -55
- package/src/filter.test.ts +0 -84
- package/src/filter.ts +0 -64
- package/src/fixtures.ts +0 -35
- package/src/formatter.test.ts +0 -532
- package/src/formatter.ts +0 -933
- package/src/level.test.ts +0 -71
- package/src/level.ts +0 -86
- package/src/logger.test.ts +0 -1052
- package/src/logger.ts +0 -1485
- package/src/mod.ts +0 -59
- package/src/record.ts +0 -49
- package/src/sink.test.ts +0 -1708
- package/src/sink.ts +0 -715
- package/src/util.deno.ts +0 -19
- package/src/util.node.ts +0 -12
- package/src/util.ts +0 -11
- package/tsdown.config.ts +0 -24
package/src/sink.test.ts
DELETED
|
@@ -1,1708 +0,0 @@
|
|
|
1
|
-
import { suite } from "@alinea/suite";
|
|
2
|
-
import { assert } from "@std/assert/assert";
|
|
3
|
-
import { assertEquals } from "@std/assert/equals";
|
|
4
|
-
import { assertInstanceOf } from "@std/assert/instance-of";
|
|
5
|
-
import { assertThrows } from "@std/assert/throws";
|
|
6
|
-
import { delay } from "@std/async/delay";
|
|
7
|
-
import makeConsoleMock from "consolemock";
|
|
8
|
-
import { debug, error, fatal, info, trace, warning } from "./fixtures.ts";
|
|
9
|
-
import { defaultTextFormatter } from "./formatter.ts";
|
|
10
|
-
import type { LogLevel } from "./level.ts";
|
|
11
|
-
import type { LogRecord } from "./record.ts";
|
|
12
|
-
import {
|
|
13
|
-
type AsyncSink,
|
|
14
|
-
fingersCrossed,
|
|
15
|
-
fromAsyncSink,
|
|
16
|
-
getConsoleSink,
|
|
17
|
-
getStreamSink,
|
|
18
|
-
type Sink,
|
|
19
|
-
withFilter,
|
|
20
|
-
} from "./sink.ts";
|
|
21
|
-
|
|
22
|
-
const test = suite(import.meta);
|
|
23
|
-
|
|
24
|
-
test("withFilter()", () => {
|
|
25
|
-
const buffer: LogRecord[] = [];
|
|
26
|
-
const sink = withFilter(buffer.push.bind(buffer), "warning");
|
|
27
|
-
sink(trace);
|
|
28
|
-
sink(debug);
|
|
29
|
-
sink(info);
|
|
30
|
-
sink(warning);
|
|
31
|
-
sink(error);
|
|
32
|
-
sink(fatal);
|
|
33
|
-
assertEquals(buffer, [warning, error, fatal]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
interface ConsoleMock extends Console {
|
|
37
|
-
history(): unknown[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
test("getStreamSink()", async () => {
|
|
41
|
-
let buffer: string = "";
|
|
42
|
-
const decoder = new TextDecoder();
|
|
43
|
-
const sink = getStreamSink(
|
|
44
|
-
new WritableStream({
|
|
45
|
-
write(chunk: Uint8Array) {
|
|
46
|
-
buffer += decoder.decode(chunk);
|
|
47
|
-
return Promise.resolve();
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
sink(trace);
|
|
52
|
-
sink(debug);
|
|
53
|
-
sink(info);
|
|
54
|
-
sink(warning);
|
|
55
|
-
sink(error);
|
|
56
|
-
sink(fatal);
|
|
57
|
-
await sink[Symbol.asyncDispose]();
|
|
58
|
-
assertEquals(
|
|
59
|
-
buffer,
|
|
60
|
-
`\
|
|
61
|
-
2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
62
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
63
|
-
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
64
|
-
2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
|
|
65
|
-
2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
|
|
66
|
-
2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
|
|
67
|
-
`,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("getStreamSink() with nonBlocking - simple boolean", async () => {
|
|
72
|
-
let buffer: string = "";
|
|
73
|
-
const decoder = new TextDecoder();
|
|
74
|
-
const sink = getStreamSink(
|
|
75
|
-
new WritableStream({
|
|
76
|
-
write(chunk: Uint8Array) {
|
|
77
|
-
buffer += decoder.decode(chunk);
|
|
78
|
-
return Promise.resolve();
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
{ nonBlocking: true },
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Check that it returns AsyncDisposable
|
|
85
|
-
assertInstanceOf(sink, Function);
|
|
86
|
-
assert(Symbol.asyncDispose in sink);
|
|
87
|
-
|
|
88
|
-
// Add records - they should not be written immediately
|
|
89
|
-
sink(trace);
|
|
90
|
-
sink(debug);
|
|
91
|
-
assertEquals(buffer, ""); // Not written yet
|
|
92
|
-
|
|
93
|
-
// Wait for flush interval (default 100ms)
|
|
94
|
-
await delay(150);
|
|
95
|
-
assertEquals(
|
|
96
|
-
buffer,
|
|
97
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
98
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
99
|
-
`,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
await sink[Symbol.asyncDispose]();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("getStreamSink() with nonBlocking - custom buffer config", async () => {
|
|
106
|
-
let buffer: string = "";
|
|
107
|
-
const decoder = new TextDecoder();
|
|
108
|
-
const sink = getStreamSink(
|
|
109
|
-
new WritableStream({
|
|
110
|
-
write(chunk: Uint8Array) {
|
|
111
|
-
buffer += decoder.decode(chunk);
|
|
112
|
-
return Promise.resolve();
|
|
113
|
-
},
|
|
114
|
-
}),
|
|
115
|
-
{
|
|
116
|
-
nonBlocking: {
|
|
117
|
-
bufferSize: 2,
|
|
118
|
-
flushInterval: 50,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Add records up to buffer size
|
|
124
|
-
sink(trace);
|
|
125
|
-
assertEquals(buffer, ""); // Not flushed yet
|
|
126
|
-
|
|
127
|
-
sink(debug); // This should trigger immediate flush (buffer size = 2)
|
|
128
|
-
await delay(10); // Small delay for async flush
|
|
129
|
-
assertEquals(
|
|
130
|
-
buffer,
|
|
131
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
132
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
133
|
-
`,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Add more records
|
|
137
|
-
const prevLength = buffer.length;
|
|
138
|
-
sink(info);
|
|
139
|
-
assertEquals(buffer.length, prevLength); // Not flushed yet
|
|
140
|
-
|
|
141
|
-
// Wait for flush interval
|
|
142
|
-
await delay(60);
|
|
143
|
-
assertEquals(
|
|
144
|
-
buffer.substring(prevLength),
|
|
145
|
-
`2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
146
|
-
`,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
await sink[Symbol.asyncDispose]();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("getStreamSink() with nonBlocking - no operations after dispose", async () => {
|
|
153
|
-
let buffer: string = "";
|
|
154
|
-
const decoder = new TextDecoder();
|
|
155
|
-
const sink = getStreamSink(
|
|
156
|
-
new WritableStream({
|
|
157
|
-
write(chunk: Uint8Array) {
|
|
158
|
-
buffer += decoder.decode(chunk);
|
|
159
|
-
return Promise.resolve();
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
162
|
-
{ nonBlocking: true },
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// Dispose immediately
|
|
166
|
-
await sink[Symbol.asyncDispose]();
|
|
167
|
-
|
|
168
|
-
// Try to add records after dispose
|
|
169
|
-
sink(trace);
|
|
170
|
-
sink(debug);
|
|
171
|
-
|
|
172
|
-
// No records should be written
|
|
173
|
-
assertEquals(buffer, "");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("getStreamSink() with nonBlocking - error handling", async () => {
|
|
177
|
-
const sink = getStreamSink(
|
|
178
|
-
new WritableStream({
|
|
179
|
-
write() {
|
|
180
|
-
return Promise.reject(new Error("Write error"));
|
|
181
|
-
},
|
|
182
|
-
}),
|
|
183
|
-
{ nonBlocking: true },
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Should not throw when adding records
|
|
187
|
-
sink(trace);
|
|
188
|
-
sink(info);
|
|
189
|
-
sink(error);
|
|
190
|
-
|
|
191
|
-
// Wait for flush - errors should be silently ignored
|
|
192
|
-
await delay(150);
|
|
193
|
-
|
|
194
|
-
// Dispose - should not throw
|
|
195
|
-
await sink[Symbol.asyncDispose]();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("getStreamSink() with nonBlocking - flush on dispose", async () => {
|
|
199
|
-
let buffer: string = "";
|
|
200
|
-
const decoder = new TextDecoder();
|
|
201
|
-
const sink = getStreamSink(
|
|
202
|
-
new WritableStream({
|
|
203
|
-
write(chunk: Uint8Array) {
|
|
204
|
-
buffer += decoder.decode(chunk);
|
|
205
|
-
return Promise.resolve();
|
|
206
|
-
},
|
|
207
|
-
}),
|
|
208
|
-
{
|
|
209
|
-
nonBlocking: {
|
|
210
|
-
bufferSize: 100,
|
|
211
|
-
flushInterval: 5000, // Very long interval
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// Add records
|
|
217
|
-
sink(trace);
|
|
218
|
-
sink(debug);
|
|
219
|
-
sink(info);
|
|
220
|
-
assertEquals(buffer, ""); // Not flushed yet due to large buffer and long interval
|
|
221
|
-
|
|
222
|
-
// Dispose should flush all remaining records
|
|
223
|
-
await sink[Symbol.asyncDispose]();
|
|
224
|
-
assertEquals(
|
|
225
|
-
buffer,
|
|
226
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
227
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
228
|
-
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
229
|
-
`,
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("getStreamSink() with nonBlocking - buffer overflow protection", async () => {
|
|
234
|
-
let buffer: string = "";
|
|
235
|
-
const decoder = new TextDecoder();
|
|
236
|
-
let recordsReceived = 0;
|
|
237
|
-
const sink = getStreamSink(
|
|
238
|
-
new WritableStream({
|
|
239
|
-
write(chunk: Uint8Array) {
|
|
240
|
-
const text = decoder.decode(chunk);
|
|
241
|
-
buffer += text;
|
|
242
|
-
// Count how many log records we actually receive
|
|
243
|
-
recordsReceived += text.split("\n").filter((line) =>
|
|
244
|
-
line.trim() !== ""
|
|
245
|
-
).length;
|
|
246
|
-
return Promise.resolve();
|
|
247
|
-
},
|
|
248
|
-
}),
|
|
249
|
-
{
|
|
250
|
-
nonBlocking: {
|
|
251
|
-
bufferSize: 3,
|
|
252
|
-
flushInterval: 50, // Short interval to ensure flushes happen
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Add many more records than maxBufferSize (6) very rapidly
|
|
258
|
-
// This should trigger multiple flushes and potentially overflow protection
|
|
259
|
-
for (let i = 0; i < 20; i++) {
|
|
260
|
-
sink(trace);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Wait for all flushes to complete
|
|
264
|
-
await delay(200);
|
|
265
|
-
|
|
266
|
-
// Force final flush
|
|
267
|
-
await sink[Symbol.asyncDispose]();
|
|
268
|
-
|
|
269
|
-
// Due to overflow protection, we should receive fewer than 20 records
|
|
270
|
-
// The exact number depends on timing, but some should be dropped
|
|
271
|
-
assert(
|
|
272
|
-
recordsReceived < 20,
|
|
273
|
-
`Expected < 20 records due to potential overflow, got ${recordsReceived}`,
|
|
274
|
-
);
|
|
275
|
-
assert(recordsReceived > 0, "Expected some records to be logged");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("getStreamSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
279
|
-
let buffer: string = "";
|
|
280
|
-
const decoder = new TextDecoder();
|
|
281
|
-
const sink = getStreamSink(
|
|
282
|
-
new WritableStream({
|
|
283
|
-
write(chunk: Uint8Array) {
|
|
284
|
-
buffer += decoder.decode(chunk);
|
|
285
|
-
return Promise.resolve();
|
|
286
|
-
},
|
|
287
|
-
}),
|
|
288
|
-
{
|
|
289
|
-
nonBlocking: {
|
|
290
|
-
bufferSize: 3,
|
|
291
|
-
flushInterval: 50,
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
// Simulate rapid logging - this should not block
|
|
297
|
-
const startTime = performance.now();
|
|
298
|
-
for (let i = 0; i < 100; i++) {
|
|
299
|
-
sink(trace);
|
|
300
|
-
}
|
|
301
|
-
const endTime = performance.now();
|
|
302
|
-
|
|
303
|
-
// Adding logs should be very fast (non-blocking)
|
|
304
|
-
const duration = endTime - startTime;
|
|
305
|
-
assert(
|
|
306
|
-
duration < 100,
|
|
307
|
-
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Wait for flushes to complete
|
|
311
|
-
await delay(200);
|
|
312
|
-
|
|
313
|
-
// Should have logged some records
|
|
314
|
-
assert(buffer.length > 0, "Expected some records to be logged");
|
|
315
|
-
|
|
316
|
-
await sink[Symbol.asyncDispose]();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("getConsoleSink()", () => {
|
|
320
|
-
// @ts-ignore: consolemock is not typed
|
|
321
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
322
|
-
const sink = getConsoleSink({ console: mock });
|
|
323
|
-
sink(trace);
|
|
324
|
-
sink(debug);
|
|
325
|
-
sink(info);
|
|
326
|
-
sink(warning);
|
|
327
|
-
sink(error);
|
|
328
|
-
sink(fatal);
|
|
329
|
-
assertEquals(mock.history(), [
|
|
330
|
-
{
|
|
331
|
-
DEBUG: [
|
|
332
|
-
"%c22:13:20.000 %cTRC%c %cmy-app·junk %cHello, %o & %o!",
|
|
333
|
-
"color: gray;",
|
|
334
|
-
"background-color: gray; color: white;",
|
|
335
|
-
"background-color: default;",
|
|
336
|
-
"color: gray;",
|
|
337
|
-
"color: default;",
|
|
338
|
-
123,
|
|
339
|
-
456,
|
|
340
|
-
],
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
DEBUG: [
|
|
344
|
-
"%c22:13:20.000 %cDBG%c %cmy-app·junk %cHello, %o & %o!",
|
|
345
|
-
"color: gray;",
|
|
346
|
-
"background-color: gray; color: white;",
|
|
347
|
-
"background-color: default;",
|
|
348
|
-
"color: gray;",
|
|
349
|
-
"color: default;",
|
|
350
|
-
123,
|
|
351
|
-
456,
|
|
352
|
-
],
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
INFO: [
|
|
356
|
-
"%c22:13:20.000 %cINF%c %cmy-app·junk %cHello, %o & %o!",
|
|
357
|
-
"color: gray;",
|
|
358
|
-
"background-color: white; color: black;",
|
|
359
|
-
"background-color: default;",
|
|
360
|
-
"color: gray;",
|
|
361
|
-
"color: default;",
|
|
362
|
-
123,
|
|
363
|
-
456,
|
|
364
|
-
],
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
WARN: [
|
|
368
|
-
"%c22:13:20.000 %cWRN%c %cmy-app·junk %cHello, %o & %o!",
|
|
369
|
-
"color: gray;",
|
|
370
|
-
"background-color: orange; color: black;",
|
|
371
|
-
"background-color: default;",
|
|
372
|
-
"color: gray;",
|
|
373
|
-
"color: default;",
|
|
374
|
-
123,
|
|
375
|
-
456,
|
|
376
|
-
],
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
ERROR: [
|
|
380
|
-
"%c22:13:20.000 %cERR%c %cmy-app·junk %cHello, %o & %o!",
|
|
381
|
-
"color: gray;",
|
|
382
|
-
"background-color: red; color: white;",
|
|
383
|
-
"background-color: default;",
|
|
384
|
-
"color: gray;",
|
|
385
|
-
"color: default;",
|
|
386
|
-
123,
|
|
387
|
-
456,
|
|
388
|
-
],
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
ERROR: [
|
|
392
|
-
"%c22:13:20.000 %cFTL%c %cmy-app·junk %cHello, %o & %o!",
|
|
393
|
-
"color: gray;",
|
|
394
|
-
"background-color: maroon; color: white;",
|
|
395
|
-
"background-color: default;",
|
|
396
|
-
"color: gray;",
|
|
397
|
-
"color: default;",
|
|
398
|
-
123,
|
|
399
|
-
456,
|
|
400
|
-
],
|
|
401
|
-
},
|
|
402
|
-
]);
|
|
403
|
-
|
|
404
|
-
assertThrows(
|
|
405
|
-
() => sink({ ...info, level: "invalid" as LogLevel }),
|
|
406
|
-
TypeError,
|
|
407
|
-
"Invalid log level: invalid.",
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// @ts-ignore: consolemock is not typed
|
|
411
|
-
const mock2: ConsoleMock = makeConsoleMock();
|
|
412
|
-
const sink2 = getConsoleSink({
|
|
413
|
-
console: mock2,
|
|
414
|
-
formatter: defaultTextFormatter,
|
|
415
|
-
});
|
|
416
|
-
sink2(trace);
|
|
417
|
-
sink2(debug);
|
|
418
|
-
sink2(info);
|
|
419
|
-
sink2(warning);
|
|
420
|
-
sink2(error);
|
|
421
|
-
sink2(fatal);
|
|
422
|
-
assertEquals(mock2.history(), [
|
|
423
|
-
{
|
|
424
|
-
DEBUG: [
|
|
425
|
-
"2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!",
|
|
426
|
-
],
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
DEBUG: [
|
|
430
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!",
|
|
431
|
-
],
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
INFO: [
|
|
435
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!",
|
|
436
|
-
],
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
WARN: [
|
|
440
|
-
"2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!",
|
|
441
|
-
],
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
ERROR: [
|
|
445
|
-
"2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!",
|
|
446
|
-
],
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
ERROR: [
|
|
450
|
-
"2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!",
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
]);
|
|
454
|
-
|
|
455
|
-
// @ts-ignore: consolemock is not typed
|
|
456
|
-
const mock3: ConsoleMock = makeConsoleMock();
|
|
457
|
-
const sink3 = getConsoleSink({
|
|
458
|
-
console: mock3,
|
|
459
|
-
levelMap: {
|
|
460
|
-
trace: "log",
|
|
461
|
-
debug: "log",
|
|
462
|
-
info: "log",
|
|
463
|
-
warning: "log",
|
|
464
|
-
error: "log",
|
|
465
|
-
fatal: "log",
|
|
466
|
-
},
|
|
467
|
-
formatter: defaultTextFormatter,
|
|
468
|
-
});
|
|
469
|
-
sink3(trace);
|
|
470
|
-
sink3(debug);
|
|
471
|
-
sink3(info);
|
|
472
|
-
sink3(warning);
|
|
473
|
-
sink3(error);
|
|
474
|
-
sink3(fatal);
|
|
475
|
-
assertEquals(mock3.history(), [
|
|
476
|
-
{
|
|
477
|
-
LOG: [
|
|
478
|
-
"2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!",
|
|
479
|
-
],
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
LOG: [
|
|
483
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!",
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
LOG: [
|
|
488
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!",
|
|
489
|
-
],
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
LOG: [
|
|
493
|
-
"2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!",
|
|
494
|
-
],
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
LOG: [
|
|
498
|
-
"2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!",
|
|
499
|
-
],
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
LOG: [
|
|
503
|
-
"2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!",
|
|
504
|
-
],
|
|
505
|
-
},
|
|
506
|
-
]);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("getConsoleSink() with nonBlocking - simple boolean", async () => {
|
|
510
|
-
// @ts-ignore: consolemock is not typed
|
|
511
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
512
|
-
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
513
|
-
|
|
514
|
-
// Check that it returns a Disposable
|
|
515
|
-
assertInstanceOf(sink, Function);
|
|
516
|
-
assert(Symbol.dispose in sink);
|
|
517
|
-
|
|
518
|
-
// Add records - they should not be logged immediately
|
|
519
|
-
sink(trace);
|
|
520
|
-
sink(debug);
|
|
521
|
-
assertEquals(mock.history().length, 0); // Not logged yet
|
|
522
|
-
|
|
523
|
-
// Wait for flush interval (default 100ms)
|
|
524
|
-
await delay(150);
|
|
525
|
-
assertEquals(mock.history().length, 2); // Now they should be logged
|
|
526
|
-
|
|
527
|
-
// Dispose the sink
|
|
528
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test("getConsoleSink() with nonBlocking - custom buffer config", async () => {
|
|
532
|
-
// @ts-ignore: consolemock is not typed
|
|
533
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
534
|
-
const sink = getConsoleSink({
|
|
535
|
-
console: mock,
|
|
536
|
-
nonBlocking: {
|
|
537
|
-
bufferSize: 3,
|
|
538
|
-
flushInterval: 50,
|
|
539
|
-
},
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// Add records up to buffer size
|
|
543
|
-
sink(trace);
|
|
544
|
-
sink(debug);
|
|
545
|
-
assertEquals(mock.history().length, 0); // Not flushed yet
|
|
546
|
-
|
|
547
|
-
sink(info); // This should trigger scheduled flush (buffer size = 3)
|
|
548
|
-
await delay(10); // Wait for scheduled flush to execute
|
|
549
|
-
assertEquals(mock.history().length, 3); // Flushed due to buffer size
|
|
550
|
-
|
|
551
|
-
// Add more records
|
|
552
|
-
sink(warning);
|
|
553
|
-
assertEquals(mock.history().length, 3); // Not flushed yet
|
|
554
|
-
|
|
555
|
-
// Wait for flush interval
|
|
556
|
-
await delay(60);
|
|
557
|
-
assertEquals(mock.history().length, 4); // Flushed due to interval
|
|
558
|
-
|
|
559
|
-
// Dispose and check remaining records are flushed
|
|
560
|
-
sink(error);
|
|
561
|
-
sink(fatal);
|
|
562
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
563
|
-
assertEquals(mock.history().length, 6); // All records flushed on dispose
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
test("getConsoleSink() with nonBlocking - no operations after dispose", () => {
|
|
567
|
-
// @ts-ignore: consolemock is not typed
|
|
568
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
569
|
-
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
570
|
-
|
|
571
|
-
// Dispose immediately
|
|
572
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
573
|
-
|
|
574
|
-
// Try to add records after dispose
|
|
575
|
-
sink(trace);
|
|
576
|
-
sink(debug);
|
|
577
|
-
|
|
578
|
-
// No records should be logged
|
|
579
|
-
assertEquals(mock.history().length, 0);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test("getConsoleSink() with nonBlocking - error handling", async () => {
|
|
583
|
-
const errorConsole = {
|
|
584
|
-
...console,
|
|
585
|
-
debug: () => {
|
|
586
|
-
throw new Error("Console error");
|
|
587
|
-
},
|
|
588
|
-
info: () => {
|
|
589
|
-
throw new Error("Console error");
|
|
590
|
-
},
|
|
591
|
-
warn: () => {
|
|
592
|
-
throw new Error("Console error");
|
|
593
|
-
},
|
|
594
|
-
error: () => {
|
|
595
|
-
throw new Error("Console error");
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const sink = getConsoleSink({
|
|
600
|
-
console: errorConsole,
|
|
601
|
-
nonBlocking: true,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// Should not throw when adding records
|
|
605
|
-
sink(trace);
|
|
606
|
-
sink(info);
|
|
607
|
-
sink(error);
|
|
608
|
-
|
|
609
|
-
// Wait for flush - errors should be silently ignored
|
|
610
|
-
await delay(150);
|
|
611
|
-
|
|
612
|
-
// Dispose - should not throw
|
|
613
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
test("getConsoleSink() with nonBlocking - buffer overflow protection", async () => {
|
|
617
|
-
// @ts-ignore: consolemock is not typed
|
|
618
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
619
|
-
const sink = getConsoleSink({
|
|
620
|
-
console: mock,
|
|
621
|
-
nonBlocking: {
|
|
622
|
-
bufferSize: 5,
|
|
623
|
-
flushInterval: 1000, // Long interval to prevent automatic flushing
|
|
624
|
-
},
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
// Add more records than 2x buffer size (which should trigger overflow protection)
|
|
628
|
-
for (let i = 0; i < 12; i++) {
|
|
629
|
-
sink(trace);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Should have dropped oldest records, keeping buffer size manageable
|
|
633
|
-
// Wait a bit for any scheduled flushes
|
|
634
|
-
await delay(10);
|
|
635
|
-
|
|
636
|
-
// Force flush by disposing
|
|
637
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
638
|
-
|
|
639
|
-
// Should have logged records, but not more than maxBufferSize (10)
|
|
640
|
-
const historyLength = mock.history().length;
|
|
641
|
-
assert(historyLength <= 10, `Expected <= 10 records, got ${historyLength}`);
|
|
642
|
-
assert(historyLength > 0, "Expected some records to be logged");
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
test("getConsoleSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
646
|
-
// @ts-ignore: consolemock is not typed
|
|
647
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
648
|
-
const sink = getConsoleSink({
|
|
649
|
-
console: mock,
|
|
650
|
-
nonBlocking: {
|
|
651
|
-
bufferSize: 3,
|
|
652
|
-
flushInterval: 50,
|
|
653
|
-
},
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
// Simulate rapid logging - this should not block
|
|
657
|
-
const startTime = performance.now();
|
|
658
|
-
for (let i = 0; i < 100; i++) {
|
|
659
|
-
sink(trace);
|
|
660
|
-
}
|
|
661
|
-
const endTime = performance.now();
|
|
662
|
-
|
|
663
|
-
// Adding logs should be very fast (non-blocking)
|
|
664
|
-
const duration = endTime - startTime;
|
|
665
|
-
assert(
|
|
666
|
-
duration < 100,
|
|
667
|
-
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
// Wait for flushes to complete
|
|
671
|
-
await delay(200);
|
|
672
|
-
|
|
673
|
-
// Should have logged some records
|
|
674
|
-
assert(mock.history().length > 0, "Expected some records to be logged");
|
|
675
|
-
|
|
676
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
test("fromAsyncSink() - basic functionality", async () => {
|
|
680
|
-
const buffer: LogRecord[] = [];
|
|
681
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
682
|
-
await delay(10);
|
|
683
|
-
buffer.push(record);
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
const sink = fromAsyncSink(asyncSink);
|
|
687
|
-
|
|
688
|
-
sink(trace);
|
|
689
|
-
sink(debug);
|
|
690
|
-
sink(info);
|
|
691
|
-
|
|
692
|
-
// Records should not be in buffer immediately
|
|
693
|
-
assertEquals(buffer.length, 0);
|
|
694
|
-
|
|
695
|
-
// Wait for async operations to complete
|
|
696
|
-
await sink[Symbol.asyncDispose]();
|
|
697
|
-
|
|
698
|
-
// All records should be in buffer in order
|
|
699
|
-
assertEquals(buffer.length, 3);
|
|
700
|
-
assertEquals(buffer, [trace, debug, info]);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
test("fromAsyncSink() - promise chaining preserves order", async () => {
|
|
704
|
-
const buffer: LogRecord[] = [];
|
|
705
|
-
const delays = [50, 10, 30]; // Different delays for each call
|
|
706
|
-
let callIndex = 0;
|
|
707
|
-
|
|
708
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
709
|
-
const delayTime = delays[callIndex % delays.length];
|
|
710
|
-
callIndex++;
|
|
711
|
-
await delay(delayTime);
|
|
712
|
-
buffer.push(record);
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
const sink = fromAsyncSink(asyncSink);
|
|
716
|
-
|
|
717
|
-
sink(trace);
|
|
718
|
-
sink(debug);
|
|
719
|
-
sink(info);
|
|
720
|
-
|
|
721
|
-
await sink[Symbol.asyncDispose]();
|
|
722
|
-
|
|
723
|
-
// Despite different delays, order should be preserved
|
|
724
|
-
assertEquals(buffer.length, 3);
|
|
725
|
-
assertEquals(buffer, [trace, debug, info]);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
test("fromAsyncSink() - error handling", async () => {
|
|
729
|
-
const buffer: LogRecord[] = [];
|
|
730
|
-
let errorCount = 0;
|
|
731
|
-
|
|
732
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
733
|
-
if (record.level === "error") {
|
|
734
|
-
errorCount++;
|
|
735
|
-
throw new Error("Async sink error");
|
|
736
|
-
}
|
|
737
|
-
await Promise.resolve(); // Ensure it's async
|
|
738
|
-
buffer.push(record);
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const sink = fromAsyncSink(asyncSink);
|
|
742
|
-
|
|
743
|
-
sink(trace);
|
|
744
|
-
sink(error); // This will throw in async sink
|
|
745
|
-
sink(info);
|
|
746
|
-
|
|
747
|
-
await sink[Symbol.asyncDispose]();
|
|
748
|
-
|
|
749
|
-
// Error should be caught and not break the chain
|
|
750
|
-
assertEquals(errorCount, 1);
|
|
751
|
-
assertEquals(buffer.length, 2);
|
|
752
|
-
assertEquals(buffer, [trace, info]);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
test("fromAsyncSink() - multiple dispose calls", async () => {
|
|
756
|
-
const buffer: LogRecord[] = [];
|
|
757
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
758
|
-
await delay(10);
|
|
759
|
-
buffer.push(record);
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const sink = fromAsyncSink(asyncSink);
|
|
763
|
-
|
|
764
|
-
sink(trace);
|
|
765
|
-
sink(debug);
|
|
766
|
-
|
|
767
|
-
// First dispose
|
|
768
|
-
await sink[Symbol.asyncDispose]();
|
|
769
|
-
assertEquals(buffer.length, 2);
|
|
770
|
-
|
|
771
|
-
// Second dispose should be safe
|
|
772
|
-
await sink[Symbol.asyncDispose]();
|
|
773
|
-
assertEquals(buffer.length, 2);
|
|
774
|
-
|
|
775
|
-
// Third dispose should be safe
|
|
776
|
-
await sink[Symbol.asyncDispose]();
|
|
777
|
-
assertEquals(buffer.length, 2);
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
test("fromAsyncSink() - concurrent calls", async () => {
|
|
781
|
-
const buffer: LogRecord[] = [];
|
|
782
|
-
let concurrentCalls = 0;
|
|
783
|
-
let maxConcurrentCalls = 0;
|
|
784
|
-
|
|
785
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
786
|
-
concurrentCalls++;
|
|
787
|
-
maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
|
|
788
|
-
await delay(20);
|
|
789
|
-
buffer.push(record);
|
|
790
|
-
concurrentCalls--;
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const sink = fromAsyncSink(asyncSink);
|
|
794
|
-
|
|
795
|
-
// Fire multiple calls rapidly
|
|
796
|
-
for (let i = 0; i < 5; i++) {
|
|
797
|
-
sink(trace);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
await sink[Symbol.asyncDispose]();
|
|
801
|
-
|
|
802
|
-
// Due to promise chaining, max concurrent calls should be 1
|
|
803
|
-
assertEquals(maxConcurrentCalls, 1);
|
|
804
|
-
assertEquals(buffer.length, 5);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
test("fromAsyncSink() - works with synchronous exceptions", async () => {
|
|
808
|
-
const buffer: LogRecord[] = [];
|
|
809
|
-
let errorCount = 0;
|
|
810
|
-
|
|
811
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
812
|
-
if (record.level === "fatal") {
|
|
813
|
-
errorCount++;
|
|
814
|
-
// Synchronous throw before any await
|
|
815
|
-
throw new Error("Sync error in async sink");
|
|
816
|
-
}
|
|
817
|
-
await delay(10);
|
|
818
|
-
buffer.push(record);
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const sink = fromAsyncSink(asyncSink);
|
|
822
|
-
|
|
823
|
-
sink(trace);
|
|
824
|
-
sink(fatal); // This will throw synchronously in async sink
|
|
825
|
-
sink(info);
|
|
826
|
-
|
|
827
|
-
await sink[Symbol.asyncDispose]();
|
|
828
|
-
|
|
829
|
-
// Error should still be caught
|
|
830
|
-
assertEquals(errorCount, 1);
|
|
831
|
-
assertEquals(buffer.length, 2);
|
|
832
|
-
assertEquals(buffer, [trace, info]);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
test("fromAsyncSink() - very long async operations", async () => {
|
|
836
|
-
const buffer: LogRecord[] = [];
|
|
837
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
838
|
-
await delay(100); // Longer delay
|
|
839
|
-
buffer.push(record);
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
const sink = fromAsyncSink(asyncSink);
|
|
843
|
-
|
|
844
|
-
sink(trace);
|
|
845
|
-
sink(debug);
|
|
846
|
-
|
|
847
|
-
// Don't wait, just dispose immediately
|
|
848
|
-
const disposePromise = sink[Symbol.asyncDispose]();
|
|
849
|
-
|
|
850
|
-
// Buffer should still be empty
|
|
851
|
-
assertEquals(buffer.length, 0);
|
|
852
|
-
|
|
853
|
-
// Wait for dispose to complete
|
|
854
|
-
await disposePromise;
|
|
855
|
-
|
|
856
|
-
// Now all records should be processed
|
|
857
|
-
assertEquals(buffer.length, 2);
|
|
858
|
-
assertEquals(buffer, [trace, debug]);
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
test("fromAsyncSink() - empty async sink", async () => {
|
|
862
|
-
const asyncSink: AsyncSink = async () => {
|
|
863
|
-
// Do nothing
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
const sink = fromAsyncSink(asyncSink);
|
|
867
|
-
|
|
868
|
-
// Should not throw
|
|
869
|
-
sink(trace);
|
|
870
|
-
sink(debug);
|
|
871
|
-
|
|
872
|
-
await sink[Symbol.asyncDispose]();
|
|
873
|
-
|
|
874
|
-
// Test passes if no errors thrown
|
|
875
|
-
assert(true);
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
test("fingersCrossed() - basic functionality", () => {
|
|
879
|
-
const buffer: LogRecord[] = [];
|
|
880
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
881
|
-
|
|
882
|
-
// Add debug and info logs - should be buffered
|
|
883
|
-
sink(trace);
|
|
884
|
-
sink(debug);
|
|
885
|
-
sink(info);
|
|
886
|
-
assertEquals(buffer.length, 0); // Not flushed yet
|
|
887
|
-
|
|
888
|
-
// Add warning - still buffered (default trigger is error)
|
|
889
|
-
sink(warning);
|
|
890
|
-
assertEquals(buffer.length, 0);
|
|
891
|
-
|
|
892
|
-
// Add error - should trigger flush
|
|
893
|
-
sink(error);
|
|
894
|
-
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
895
|
-
|
|
896
|
-
// After trigger, logs pass through directly
|
|
897
|
-
sink(fatal);
|
|
898
|
-
assertEquals(buffer, [trace, debug, info, warning, error, fatal]);
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
test("fingersCrossed() - custom trigger level", () => {
|
|
902
|
-
const buffer: LogRecord[] = [];
|
|
903
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
904
|
-
triggerLevel: "warning",
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// Add logs below warning
|
|
908
|
-
sink(trace);
|
|
909
|
-
sink(debug);
|
|
910
|
-
sink(info);
|
|
911
|
-
assertEquals(buffer.length, 0);
|
|
912
|
-
|
|
913
|
-
// Warning should trigger flush
|
|
914
|
-
sink(warning);
|
|
915
|
-
assertEquals(buffer, [trace, debug, info, warning]);
|
|
916
|
-
|
|
917
|
-
// Subsequent logs pass through
|
|
918
|
-
sink(error);
|
|
919
|
-
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
test("fingersCrossed() - buffer overflow protection", () => {
|
|
923
|
-
const buffer: LogRecord[] = [];
|
|
924
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
925
|
-
maxBufferSize: 3,
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// Add more logs than buffer size
|
|
929
|
-
sink(trace);
|
|
930
|
-
sink(debug);
|
|
931
|
-
sink(info);
|
|
932
|
-
sink(warning); // Should drop trace
|
|
933
|
-
assertEquals(buffer.length, 0); // Still buffered
|
|
934
|
-
|
|
935
|
-
// Trigger flush
|
|
936
|
-
sink(error);
|
|
937
|
-
// Should only have last 3 records + error
|
|
938
|
-
assertEquals(buffer, [debug, info, warning, error]);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
test("fingersCrossed() - multiple trigger events", () => {
|
|
942
|
-
const buffer: LogRecord[] = [];
|
|
943
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
944
|
-
|
|
945
|
-
// First batch
|
|
946
|
-
sink(debug);
|
|
947
|
-
sink(info);
|
|
948
|
-
sink(error); // Trigger
|
|
949
|
-
assertEquals(buffer, [debug, info, error]);
|
|
950
|
-
|
|
951
|
-
// After trigger, everything passes through
|
|
952
|
-
sink(debug);
|
|
953
|
-
assertEquals(buffer, [debug, info, error, debug]);
|
|
954
|
-
|
|
955
|
-
sink(error); // Another error
|
|
956
|
-
assertEquals(buffer, [debug, info, error, debug, error]);
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
test("fingersCrossed() - trigger includes fatal", () => {
|
|
960
|
-
const buffer: LogRecord[] = [];
|
|
961
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
962
|
-
triggerLevel: "error",
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
sink(debug);
|
|
966
|
-
sink(info);
|
|
967
|
-
assertEquals(buffer.length, 0);
|
|
968
|
-
|
|
969
|
-
// Fatal should also trigger (since it's >= error)
|
|
970
|
-
sink(fatal);
|
|
971
|
-
assertEquals(buffer, [debug, info, fatal]);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
test("fingersCrossed() - category isolation descendant mode", () => {
|
|
975
|
-
const buffer: LogRecord[] = [];
|
|
976
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
977
|
-
isolateByCategory: "descendant",
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
// Create test records with different categories
|
|
981
|
-
const appDebug: LogRecord = {
|
|
982
|
-
...debug,
|
|
983
|
-
category: ["app"],
|
|
984
|
-
};
|
|
985
|
-
const appModuleDebug: LogRecord = {
|
|
986
|
-
...debug,
|
|
987
|
-
category: ["app", "module"],
|
|
988
|
-
};
|
|
989
|
-
const appModuleSubDebug: LogRecord = {
|
|
990
|
-
...debug,
|
|
991
|
-
category: ["app", "module", "sub"],
|
|
992
|
-
};
|
|
993
|
-
const otherDebug: LogRecord = {
|
|
994
|
-
...debug,
|
|
995
|
-
category: ["other"],
|
|
996
|
-
};
|
|
997
|
-
const appError: LogRecord = {
|
|
998
|
-
...error,
|
|
999
|
-
category: ["app"],
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
// Buffer logs in different categories
|
|
1003
|
-
sink(appDebug);
|
|
1004
|
-
sink(appModuleDebug);
|
|
1005
|
-
sink(appModuleSubDebug);
|
|
1006
|
-
sink(otherDebug);
|
|
1007
|
-
assertEquals(buffer.length, 0);
|
|
1008
|
-
|
|
1009
|
-
// Trigger in parent category
|
|
1010
|
-
sink(appError);
|
|
1011
|
-
|
|
1012
|
-
// Should flush parent and all descendants, but not other
|
|
1013
|
-
assertEquals(buffer.length, 4); // app, app.module, app.module.sub, and trigger
|
|
1014
|
-
assert(buffer.includes(appDebug));
|
|
1015
|
-
assert(buffer.includes(appModuleDebug));
|
|
1016
|
-
assert(buffer.includes(appModuleSubDebug));
|
|
1017
|
-
assert(buffer.includes(appError));
|
|
1018
|
-
assert(!buffer.includes(otherDebug));
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
test("fingersCrossed() - category isolation ancestor mode", () => {
|
|
1022
|
-
const buffer: LogRecord[] = [];
|
|
1023
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1024
|
-
isolateByCategory: "ancestor",
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
// Create test records
|
|
1028
|
-
const appDebug: LogRecord = {
|
|
1029
|
-
...debug,
|
|
1030
|
-
category: ["app"],
|
|
1031
|
-
};
|
|
1032
|
-
const appModuleDebug: LogRecord = {
|
|
1033
|
-
...debug,
|
|
1034
|
-
category: ["app", "module"],
|
|
1035
|
-
};
|
|
1036
|
-
const appModuleSubDebug: LogRecord = {
|
|
1037
|
-
...debug,
|
|
1038
|
-
category: ["app", "module", "sub"],
|
|
1039
|
-
};
|
|
1040
|
-
const appModuleSubError: LogRecord = {
|
|
1041
|
-
...error,
|
|
1042
|
-
category: ["app", "module", "sub"],
|
|
1043
|
-
};
|
|
1044
|
-
|
|
1045
|
-
// Buffer logs
|
|
1046
|
-
sink(appDebug);
|
|
1047
|
-
sink(appModuleDebug);
|
|
1048
|
-
sink(appModuleSubDebug);
|
|
1049
|
-
assertEquals(buffer.length, 0);
|
|
1050
|
-
|
|
1051
|
-
// Trigger in child category
|
|
1052
|
-
sink(appModuleSubError);
|
|
1053
|
-
|
|
1054
|
-
// Should flush child and all ancestors
|
|
1055
|
-
assertEquals(buffer.length, 4);
|
|
1056
|
-
assert(buffer.includes(appDebug));
|
|
1057
|
-
assert(buffer.includes(appModuleDebug));
|
|
1058
|
-
assert(buffer.includes(appModuleSubDebug));
|
|
1059
|
-
assert(buffer.includes(appModuleSubError));
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
test("fingersCrossed() - category isolation both mode", () => {
|
|
1063
|
-
const buffer: LogRecord[] = [];
|
|
1064
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1065
|
-
isolateByCategory: "both",
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
// Create test records
|
|
1069
|
-
const rootDebug: LogRecord = {
|
|
1070
|
-
...debug,
|
|
1071
|
-
category: ["app"],
|
|
1072
|
-
};
|
|
1073
|
-
const parentDebug: LogRecord = {
|
|
1074
|
-
...debug,
|
|
1075
|
-
category: ["app", "parent"],
|
|
1076
|
-
};
|
|
1077
|
-
const siblingDebug: LogRecord = {
|
|
1078
|
-
...debug,
|
|
1079
|
-
category: ["app", "sibling"],
|
|
1080
|
-
};
|
|
1081
|
-
const childDebug: LogRecord = {
|
|
1082
|
-
...debug,
|
|
1083
|
-
category: ["app", "parent", "child"],
|
|
1084
|
-
};
|
|
1085
|
-
const unrelatedDebug: LogRecord = {
|
|
1086
|
-
...debug,
|
|
1087
|
-
category: ["other"],
|
|
1088
|
-
};
|
|
1089
|
-
const parentError: LogRecord = {
|
|
1090
|
-
...error,
|
|
1091
|
-
category: ["app", "parent"],
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
// Buffer logs
|
|
1095
|
-
sink(rootDebug);
|
|
1096
|
-
sink(parentDebug);
|
|
1097
|
-
sink(siblingDebug);
|
|
1098
|
-
sink(childDebug);
|
|
1099
|
-
sink(unrelatedDebug);
|
|
1100
|
-
assertEquals(buffer.length, 0);
|
|
1101
|
-
|
|
1102
|
-
// Trigger in middle category
|
|
1103
|
-
sink(parentError);
|
|
1104
|
-
|
|
1105
|
-
// Should flush ancestors and descendants, but not siblings or unrelated
|
|
1106
|
-
assertEquals(buffer.length, 4);
|
|
1107
|
-
assert(buffer.includes(rootDebug)); // Ancestor
|
|
1108
|
-
assert(buffer.includes(parentDebug)); // Same
|
|
1109
|
-
assert(buffer.includes(childDebug)); // Descendant
|
|
1110
|
-
assert(buffer.includes(parentError)); // Trigger
|
|
1111
|
-
assert(!buffer.includes(siblingDebug)); // Sibling
|
|
1112
|
-
assert(!buffer.includes(unrelatedDebug)); // Unrelated
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
test("fingersCrossed() - custom category matcher", () => {
|
|
1116
|
-
const buffer: LogRecord[] = [];
|
|
1117
|
-
|
|
1118
|
-
// Custom matcher: only flush if categories share first element
|
|
1119
|
-
const customMatcher = (
|
|
1120
|
-
trigger: readonly string[],
|
|
1121
|
-
buffered: readonly string[],
|
|
1122
|
-
): boolean => {
|
|
1123
|
-
return trigger[0] === buffered[0];
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1127
|
-
isolateByCategory: customMatcher,
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
// Create test records
|
|
1131
|
-
const app1Debug: LogRecord = {
|
|
1132
|
-
...debug,
|
|
1133
|
-
category: ["app", "module1"],
|
|
1134
|
-
};
|
|
1135
|
-
const app2Debug: LogRecord = {
|
|
1136
|
-
...debug,
|
|
1137
|
-
category: ["app", "module2"],
|
|
1138
|
-
};
|
|
1139
|
-
const otherDebug: LogRecord = {
|
|
1140
|
-
...debug,
|
|
1141
|
-
category: ["other", "module"],
|
|
1142
|
-
};
|
|
1143
|
-
const appError: LogRecord = {
|
|
1144
|
-
...error,
|
|
1145
|
-
category: ["app", "module3"],
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
// Buffer logs
|
|
1149
|
-
sink(app1Debug);
|
|
1150
|
-
sink(app2Debug);
|
|
1151
|
-
sink(otherDebug);
|
|
1152
|
-
assertEquals(buffer.length, 0);
|
|
1153
|
-
|
|
1154
|
-
// Trigger
|
|
1155
|
-
sink(appError);
|
|
1156
|
-
|
|
1157
|
-
// Should flush all with same first category element
|
|
1158
|
-
assertEquals(buffer.length, 3);
|
|
1159
|
-
assert(buffer.includes(app1Debug));
|
|
1160
|
-
assert(buffer.includes(app2Debug));
|
|
1161
|
-
assert(buffer.includes(appError));
|
|
1162
|
-
assert(!buffer.includes(otherDebug));
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
test("fingersCrossed() - isolated buffers maintain separate states", () => {
|
|
1166
|
-
const buffer: LogRecord[] = [];
|
|
1167
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1168
|
-
isolateByCategory: "descendant",
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// Create test records
|
|
1172
|
-
const app1Debug: LogRecord = {
|
|
1173
|
-
...debug,
|
|
1174
|
-
category: ["app1"],
|
|
1175
|
-
};
|
|
1176
|
-
const app1Error: LogRecord = {
|
|
1177
|
-
...error,
|
|
1178
|
-
category: ["app1"],
|
|
1179
|
-
};
|
|
1180
|
-
const app2Debug: LogRecord = {
|
|
1181
|
-
...debug,
|
|
1182
|
-
category: ["app2"],
|
|
1183
|
-
};
|
|
1184
|
-
const app2Info: LogRecord = {
|
|
1185
|
-
...info,
|
|
1186
|
-
category: ["app2"],
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
// Buffer in app1
|
|
1190
|
-
sink(app1Debug);
|
|
1191
|
-
|
|
1192
|
-
// Trigger app1
|
|
1193
|
-
sink(app1Error);
|
|
1194
|
-
assertEquals(buffer, [app1Debug, app1Error]);
|
|
1195
|
-
|
|
1196
|
-
// Buffer in app2 (should still be buffering)
|
|
1197
|
-
sink(app2Debug);
|
|
1198
|
-
assertEquals(buffer, [app1Debug, app1Error]); // app2 still buffered
|
|
1199
|
-
|
|
1200
|
-
// Add more to triggered app1 (should pass through)
|
|
1201
|
-
sink(app1Debug);
|
|
1202
|
-
assertEquals(buffer, [app1Debug, app1Error, app1Debug]);
|
|
1203
|
-
|
|
1204
|
-
// app2 still buffering
|
|
1205
|
-
sink(app2Info);
|
|
1206
|
-
assertEquals(buffer, [app1Debug, app1Error, app1Debug]); // app2 still buffered
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
test("fingersCrossed() - chronological order in category isolation", () => {
|
|
1210
|
-
const buffer: LogRecord[] = [];
|
|
1211
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1212
|
-
isolateByCategory: "both",
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
// Create test records with different timestamps
|
|
1216
|
-
const app1: LogRecord = {
|
|
1217
|
-
...debug,
|
|
1218
|
-
category: ["app"],
|
|
1219
|
-
timestamp: 1000,
|
|
1220
|
-
};
|
|
1221
|
-
const app2: LogRecord = {
|
|
1222
|
-
...debug,
|
|
1223
|
-
category: ["app", "sub"],
|
|
1224
|
-
timestamp: 2000,
|
|
1225
|
-
};
|
|
1226
|
-
const app3: LogRecord = {
|
|
1227
|
-
...info,
|
|
1228
|
-
category: ["app"],
|
|
1229
|
-
timestamp: 3000,
|
|
1230
|
-
};
|
|
1231
|
-
const app4: LogRecord = {
|
|
1232
|
-
...debug,
|
|
1233
|
-
category: ["app", "sub"],
|
|
1234
|
-
timestamp: 4000,
|
|
1235
|
-
};
|
|
1236
|
-
const appError: LogRecord = {
|
|
1237
|
-
...error,
|
|
1238
|
-
category: ["app"],
|
|
1239
|
-
timestamp: 5000,
|
|
1240
|
-
};
|
|
1241
|
-
|
|
1242
|
-
// Add out of order
|
|
1243
|
-
sink(app3);
|
|
1244
|
-
sink(app1);
|
|
1245
|
-
sink(app4);
|
|
1246
|
-
sink(app2);
|
|
1247
|
-
|
|
1248
|
-
// Trigger
|
|
1249
|
-
sink(appError);
|
|
1250
|
-
|
|
1251
|
-
// Should be sorted by timestamp
|
|
1252
|
-
assertEquals(buffer, [app1, app2, app3, app4, appError]);
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
test("fingersCrossed() - empty buffer trigger", () => {
|
|
1256
|
-
const buffer: LogRecord[] = [];
|
|
1257
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1258
|
-
|
|
1259
|
-
// Trigger immediately without any buffered logs
|
|
1260
|
-
sink(error);
|
|
1261
|
-
assertEquals(buffer, [error]);
|
|
1262
|
-
|
|
1263
|
-
// Continue to pass through
|
|
1264
|
-
sink(debug);
|
|
1265
|
-
assertEquals(buffer, [error, debug]);
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
test("fingersCrossed() - buffer size per category in isolation mode", () => {
|
|
1269
|
-
const buffer: LogRecord[] = [];
|
|
1270
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1271
|
-
maxBufferSize: 2,
|
|
1272
|
-
isolateByCategory: "descendant",
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
// Create records for different categories
|
|
1276
|
-
const app1Trace: LogRecord = { ...trace, category: ["app1"] };
|
|
1277
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1278
|
-
const app1Info: LogRecord = { ...info, category: ["app1"] };
|
|
1279
|
-
const app2Trace: LogRecord = { ...trace, category: ["app2"] };
|
|
1280
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1281
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1282
|
-
|
|
1283
|
-
// Fill app1 buffer beyond max
|
|
1284
|
-
sink(app1Trace);
|
|
1285
|
-
sink(app1Debug);
|
|
1286
|
-
sink(app1Info); // Should drop app1Trace
|
|
1287
|
-
|
|
1288
|
-
// Fill app2 buffer
|
|
1289
|
-
sink(app2Trace);
|
|
1290
|
-
sink(app2Debug);
|
|
1291
|
-
|
|
1292
|
-
// Trigger app1
|
|
1293
|
-
sink(app1Error);
|
|
1294
|
-
|
|
1295
|
-
// Should only have last 2 from app1 + error
|
|
1296
|
-
assertEquals(buffer.length, 3);
|
|
1297
|
-
assert(!buffer.some((r) => r === app1Trace)); // Dropped
|
|
1298
|
-
assert(buffer.includes(app1Debug));
|
|
1299
|
-
assert(buffer.includes(app1Info));
|
|
1300
|
-
assert(buffer.includes(app1Error));
|
|
1301
|
-
// app2 records should not be flushed
|
|
1302
|
-
assert(!buffer.includes(app2Trace));
|
|
1303
|
-
assert(!buffer.includes(app2Debug));
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
test("fingersCrossed() - edge case: trigger level not in severity order", () => {
|
|
1307
|
-
const buffer: LogRecord[] = [];
|
|
1308
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1309
|
-
triggerLevel: "trace", // Lowest level triggers immediately
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
// Everything should pass through immediately
|
|
1313
|
-
sink(trace);
|
|
1314
|
-
assertEquals(buffer, [trace]);
|
|
1315
|
-
|
|
1316
|
-
sink(debug);
|
|
1317
|
-
assertEquals(buffer, [trace, debug]);
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
test("fingersCrossed() - edge case: invalid trigger level", () => {
|
|
1321
|
-
const buffer: LogRecord[] = [];
|
|
1322
|
-
|
|
1323
|
-
// Should throw TypeError during sink creation
|
|
1324
|
-
assertThrows(
|
|
1325
|
-
() => {
|
|
1326
|
-
fingersCrossed(buffer.push.bind(buffer), {
|
|
1327
|
-
triggerLevel: "invalid" as LogLevel,
|
|
1328
|
-
});
|
|
1329
|
-
},
|
|
1330
|
-
TypeError,
|
|
1331
|
-
"Invalid triggerLevel",
|
|
1332
|
-
);
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
test("fingersCrossed() - edge case: very large buffer size", () => {
|
|
1336
|
-
const buffer: LogRecord[] = [];
|
|
1337
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1338
|
-
maxBufferSize: Number.MAX_SAFE_INTEGER,
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
// Add many records
|
|
1342
|
-
for (let i = 0; i < 1000; i++) {
|
|
1343
|
-
sink(debug);
|
|
1344
|
-
}
|
|
1345
|
-
assertEquals(buffer.length, 0); // Still buffered
|
|
1346
|
-
|
|
1347
|
-
sink(error);
|
|
1348
|
-
assertEquals(buffer.length, 1001); // All 1000 + error
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
test("fingersCrossed() - edge case: zero buffer size", () => {
|
|
1352
|
-
const buffer: LogRecord[] = [];
|
|
1353
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1354
|
-
maxBufferSize: 0,
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
// Nothing should be buffered
|
|
1358
|
-
sink(debug);
|
|
1359
|
-
sink(info);
|
|
1360
|
-
assertEquals(buffer.length, 0);
|
|
1361
|
-
|
|
1362
|
-
// Trigger should still work
|
|
1363
|
-
sink(error);
|
|
1364
|
-
assertEquals(buffer, [error]); // Only the trigger
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test("fingersCrossed() - edge case: negative buffer size", () => {
|
|
1368
|
-
const buffer: LogRecord[] = [];
|
|
1369
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1370
|
-
maxBufferSize: -1,
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
// Should behave like zero
|
|
1374
|
-
sink(debug);
|
|
1375
|
-
sink(info);
|
|
1376
|
-
assertEquals(buffer.length, 0);
|
|
1377
|
-
|
|
1378
|
-
sink(error);
|
|
1379
|
-
assertEquals(buffer, [error]);
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
test("fingersCrossed() - edge case: same record logged multiple times", () => {
|
|
1383
|
-
const buffer: LogRecord[] = [];
|
|
1384
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1385
|
-
|
|
1386
|
-
// Log same record multiple times
|
|
1387
|
-
sink(debug);
|
|
1388
|
-
sink(debug);
|
|
1389
|
-
sink(debug);
|
|
1390
|
-
assertEquals(buffer.length, 0);
|
|
1391
|
-
|
|
1392
|
-
sink(error);
|
|
1393
|
-
// All instances should be preserved
|
|
1394
|
-
assertEquals(buffer.length, 4);
|
|
1395
|
-
assertEquals(buffer, [debug, debug, debug, error]);
|
|
1396
|
-
});
|
|
1397
|
-
|
|
1398
|
-
test("fingersCrossed() - edge case: empty category array", () => {
|
|
1399
|
-
const buffer: LogRecord[] = [];
|
|
1400
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1401
|
-
isolateByCategory: "both",
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
const emptyCategory: LogRecord = {
|
|
1405
|
-
...debug,
|
|
1406
|
-
category: [],
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
const normalCategory: LogRecord = {
|
|
1410
|
-
...info,
|
|
1411
|
-
category: ["app"],
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
const emptyError: LogRecord = {
|
|
1415
|
-
...error,
|
|
1416
|
-
category: [],
|
|
1417
|
-
};
|
|
1418
|
-
|
|
1419
|
-
sink(emptyCategory);
|
|
1420
|
-
sink(normalCategory);
|
|
1421
|
-
assertEquals(buffer.length, 0);
|
|
1422
|
-
|
|
1423
|
-
// Trigger with empty category
|
|
1424
|
-
sink(emptyError);
|
|
1425
|
-
|
|
1426
|
-
// Only empty category should flush (no ancestors/descendants)
|
|
1427
|
-
assertEquals(buffer.length, 2);
|
|
1428
|
-
assert(buffer.includes(emptyCategory));
|
|
1429
|
-
assert(buffer.includes(emptyError));
|
|
1430
|
-
assert(!buffer.includes(normalCategory));
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
test("fingersCrossed() - edge case: category with special characters", () => {
|
|
1434
|
-
const buffer: LogRecord[] = [];
|
|
1435
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1436
|
-
isolateByCategory: "descendant",
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
// Category with null character (our separator)
|
|
1440
|
-
const specialCategory: LogRecord = {
|
|
1441
|
-
...debug,
|
|
1442
|
-
category: ["app\0special", "sub"],
|
|
1443
|
-
};
|
|
1444
|
-
|
|
1445
|
-
const normalCategory: LogRecord = {
|
|
1446
|
-
...info,
|
|
1447
|
-
category: ["app"],
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
|
-
const specialError: LogRecord = {
|
|
1451
|
-
...error,
|
|
1452
|
-
category: ["app\0special"],
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
sink(specialCategory);
|
|
1456
|
-
sink(normalCategory);
|
|
1457
|
-
assertEquals(buffer.length, 0);
|
|
1458
|
-
|
|
1459
|
-
// Should still work correctly despite special characters
|
|
1460
|
-
sink(specialError);
|
|
1461
|
-
|
|
1462
|
-
assertEquals(buffer.length, 2);
|
|
1463
|
-
assert(buffer.includes(specialCategory));
|
|
1464
|
-
assert(buffer.includes(specialError));
|
|
1465
|
-
assert(!buffer.includes(normalCategory));
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
test("fingersCrossed() - edge case: rapid alternating triggers", () => {
|
|
1469
|
-
const buffer: LogRecord[] = [];
|
|
1470
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1471
|
-
isolateByCategory: "both",
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1475
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1476
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1477
|
-
const app2Error: LogRecord = { ...error, category: ["app2"] };
|
|
1478
|
-
|
|
1479
|
-
// Rapidly alternate between categories and triggers
|
|
1480
|
-
sink(app1Debug);
|
|
1481
|
-
sink(app2Debug);
|
|
1482
|
-
sink(app1Error); // Trigger app1
|
|
1483
|
-
assertEquals(buffer.length, 2); // app1Debug + app1Error
|
|
1484
|
-
|
|
1485
|
-
sink(app2Error); // Trigger app2
|
|
1486
|
-
assertEquals(buffer.length, 4); // Previous + app2Debug + app2Error
|
|
1487
|
-
|
|
1488
|
-
// After both triggered, everything passes through
|
|
1489
|
-
sink(app1Debug);
|
|
1490
|
-
sink(app2Debug);
|
|
1491
|
-
assertEquals(buffer.length, 6);
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
test("fingersCrossed() - edge case: custom matcher throws error", () => {
|
|
1495
|
-
const buffer: LogRecord[] = [];
|
|
1496
|
-
|
|
1497
|
-
const errorMatcher = (): boolean => {
|
|
1498
|
-
throw new Error("Matcher error");
|
|
1499
|
-
};
|
|
1500
|
-
|
|
1501
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1502
|
-
isolateByCategory: errorMatcher,
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1506
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1507
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1508
|
-
|
|
1509
|
-
sink(app1Debug);
|
|
1510
|
-
sink(app2Debug);
|
|
1511
|
-
|
|
1512
|
-
// Should handle error gracefully and still trigger
|
|
1513
|
-
try {
|
|
1514
|
-
sink(app1Error);
|
|
1515
|
-
} catch {
|
|
1516
|
-
// Should not throw to caller
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// At minimum, trigger record should be sent
|
|
1520
|
-
assert(buffer.includes(app1Error));
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
test("fingersCrossed() - edge case: circular category references", () => {
|
|
1524
|
-
const buffer: LogRecord[] = [];
|
|
1525
|
-
|
|
1526
|
-
// Custom matcher that creates circular logic
|
|
1527
|
-
const circularMatcher = (
|
|
1528
|
-
_trigger: readonly string[],
|
|
1529
|
-
_buffered: readonly string[],
|
|
1530
|
-
): boolean => {
|
|
1531
|
-
// Always return true, creating a circular flush
|
|
1532
|
-
return true;
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1536
|
-
isolateByCategory: circularMatcher,
|
|
1537
|
-
});
|
|
1538
|
-
|
|
1539
|
-
const app1: LogRecord = { ...debug, category: ["app1"] };
|
|
1540
|
-
const app2: LogRecord = { ...debug, category: ["app2"] };
|
|
1541
|
-
const app3: LogRecord = { ...debug, category: ["app3"] };
|
|
1542
|
-
const trigger: LogRecord = { ...error, category: ["trigger"] };
|
|
1543
|
-
|
|
1544
|
-
sink(app1);
|
|
1545
|
-
sink(app2);
|
|
1546
|
-
sink(app3);
|
|
1547
|
-
assertEquals(buffer.length, 0);
|
|
1548
|
-
|
|
1549
|
-
// Should flush all despite circular logic
|
|
1550
|
-
sink(trigger);
|
|
1551
|
-
assertEquals(buffer.length, 4);
|
|
1552
|
-
|
|
1553
|
-
// All buffers should be cleared after flush
|
|
1554
|
-
const newDebug: LogRecord = { ...debug, category: ["new"] };
|
|
1555
|
-
sink(newDebug);
|
|
1556
|
-
assertEquals(buffer.length, 4); // New category should be buffered
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
test("fingersCrossed() - edge case: timestamps in wrong order", () => {
|
|
1560
|
-
const buffer: LogRecord[] = [];
|
|
1561
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1562
|
-
isolateByCategory: "both",
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
const future: LogRecord = {
|
|
1566
|
-
...debug,
|
|
1567
|
-
category: ["app"],
|
|
1568
|
-
timestamp: Date.now() + 10000, // Future
|
|
1569
|
-
};
|
|
1570
|
-
|
|
1571
|
-
const past: LogRecord = {
|
|
1572
|
-
...info,
|
|
1573
|
-
category: ["app", "sub"],
|
|
1574
|
-
timestamp: Date.now() - 10000, // Past
|
|
1575
|
-
};
|
|
1576
|
-
|
|
1577
|
-
const present: LogRecord = {
|
|
1578
|
-
...warning,
|
|
1579
|
-
category: ["app"],
|
|
1580
|
-
timestamp: Date.now(),
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
const trigger: LogRecord = {
|
|
1584
|
-
...error,
|
|
1585
|
-
category: ["app"],
|
|
1586
|
-
timestamp: Date.now() + 5000,
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
// Add in random order
|
|
1590
|
-
sink(future);
|
|
1591
|
-
sink(past);
|
|
1592
|
-
sink(present);
|
|
1593
|
-
|
|
1594
|
-
// Trigger
|
|
1595
|
-
sink(trigger);
|
|
1596
|
-
|
|
1597
|
-
// Should be sorted by timestamp
|
|
1598
|
-
assertEquals(buffer[0], past);
|
|
1599
|
-
assertEquals(buffer[1], present);
|
|
1600
|
-
assertEquals(buffer[2], future);
|
|
1601
|
-
assertEquals(buffer[3], trigger);
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
|
-
test("fingersCrossed() - edge case: NaN and Infinity in timestamps", () => {
|
|
1605
|
-
const buffer: LogRecord[] = [];
|
|
1606
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1607
|
-
isolateByCategory: "both",
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
const nanTime: LogRecord = {
|
|
1611
|
-
...debug,
|
|
1612
|
-
category: ["app"],
|
|
1613
|
-
timestamp: NaN,
|
|
1614
|
-
};
|
|
1615
|
-
|
|
1616
|
-
const infinityTime: LogRecord = {
|
|
1617
|
-
...info,
|
|
1618
|
-
category: ["app"],
|
|
1619
|
-
timestamp: Infinity,
|
|
1620
|
-
};
|
|
1621
|
-
|
|
1622
|
-
const negInfinityTime: LogRecord = {
|
|
1623
|
-
...warning,
|
|
1624
|
-
category: ["app"],
|
|
1625
|
-
timestamp: -Infinity,
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
const normalTime: LogRecord = {
|
|
1629
|
-
...error,
|
|
1630
|
-
category: ["app"],
|
|
1631
|
-
timestamp: 1000,
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
sink(nanTime);
|
|
1635
|
-
sink(infinityTime);
|
|
1636
|
-
sink(negInfinityTime);
|
|
1637
|
-
|
|
1638
|
-
// Should handle special values without crashing
|
|
1639
|
-
sink(normalTime);
|
|
1640
|
-
|
|
1641
|
-
// Check all records are present (order might vary with NaN)
|
|
1642
|
-
assertEquals(buffer.length, 4);
|
|
1643
|
-
assert(buffer.includes(nanTime));
|
|
1644
|
-
assert(buffer.includes(infinityTime));
|
|
1645
|
-
assert(buffer.includes(negInfinityTime));
|
|
1646
|
-
assert(buffer.includes(normalTime));
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
test("fingersCrossed() - edge case: undefined properties in record", () => {
|
|
1650
|
-
const buffer: LogRecord[] = [];
|
|
1651
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1652
|
-
|
|
1653
|
-
const weirdRecord: LogRecord = {
|
|
1654
|
-
...debug,
|
|
1655
|
-
properties: {
|
|
1656
|
-
normal: "value",
|
|
1657
|
-
undef: undefined,
|
|
1658
|
-
nullish: null,
|
|
1659
|
-
nan: NaN,
|
|
1660
|
-
inf: Infinity,
|
|
1661
|
-
},
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
sink(weirdRecord);
|
|
1665
|
-
sink(error);
|
|
1666
|
-
|
|
1667
|
-
// Should preserve all properties as-is
|
|
1668
|
-
assertEquals(buffer[0].properties, weirdRecord.properties);
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
test("fingersCrossed() - edge case: very deep category hierarchy", () => {
|
|
1672
|
-
const buffer: LogRecord[] = [];
|
|
1673
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1674
|
-
isolateByCategory: "both",
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
// Create very deep hierarchy
|
|
1678
|
-
const deepCategory = Array.from({ length: 100 }, (_, i) => `level${i}`);
|
|
1679
|
-
const parentCategory = deepCategory.slice(0, 50);
|
|
1680
|
-
|
|
1681
|
-
const deepRecord: LogRecord = {
|
|
1682
|
-
...debug,
|
|
1683
|
-
category: deepCategory,
|
|
1684
|
-
};
|
|
1685
|
-
|
|
1686
|
-
const parentRecord: LogRecord = {
|
|
1687
|
-
...info,
|
|
1688
|
-
category: parentCategory,
|
|
1689
|
-
};
|
|
1690
|
-
|
|
1691
|
-
const deepError: LogRecord = {
|
|
1692
|
-
...error,
|
|
1693
|
-
category: deepCategory,
|
|
1694
|
-
};
|
|
1695
|
-
|
|
1696
|
-
sink(deepRecord);
|
|
1697
|
-
sink(parentRecord);
|
|
1698
|
-
assertEquals(buffer.length, 0);
|
|
1699
|
-
|
|
1700
|
-
// Should handle deep hierarchies
|
|
1701
|
-
sink(deepError);
|
|
1702
|
-
|
|
1703
|
-
// Both should flush (ancestor relationship)
|
|
1704
|
-
assertEquals(buffer.length, 3);
|
|
1705
|
-
assert(buffer.includes(deepRecord));
|
|
1706
|
-
assert(buffer.includes(parentRecord));
|
|
1707
|
-
assert(buffer.includes(deepError));
|
|
1708
|
-
});
|