Skip to content

Commit 6d01809

Browse files
committed
spike out swap to custom logger
1 parent 9d4bb38 commit 6d01809

45 files changed

Lines changed: 1568 additions & 1111 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

helpers/test-logger.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { expect } from "vitest";
2+
import { createApi as createContextApi } from "@effectionx/context-api";
3+
import { createContext, useScope } from "effection";
4+
import { logger } from "../packages/covector/src";
5+
6+
export interface TestLogEntry {
7+
msg: string;
8+
level: number;
9+
[key: string]: unknown;
10+
}
11+
12+
export function sink(): TestLogEntry[] {
13+
return [];
14+
}
15+
16+
function toMessage(message: string | object): string {
17+
if (typeof message === "string") return message;
18+
if (message && typeof message === "object" && "msg" in message) {
19+
const value = (message as { msg?: unknown }).msg;
20+
if (typeof value === "string") return value;
21+
}
22+
return JSON.stringify(message);
23+
}
24+
25+
function parseEntry(level: number, args: unknown[]): TestLogEntry {
26+
const first = args[0];
27+
const second = args[1];
28+
29+
if (
30+
first != null &&
31+
typeof first === "object" &&
32+
!Array.isArray(first) &&
33+
!(first instanceof Error)
34+
) {
35+
const bindings = first as Record<string, unknown>;
36+
const msg =
37+
typeof second === "string"
38+
? second
39+
: typeof bindings.msg === "string"
40+
? bindings.msg
41+
: toMessage(bindings as object);
42+
return {
43+
...bindings,
44+
msg,
45+
level,
46+
};
47+
}
48+
49+
if (first instanceof Error) {
50+
return {
51+
msg: first.stack ?? first.message,
52+
level,
53+
};
54+
}
55+
56+
if (typeof first === "string") {
57+
return {
58+
msg: first,
59+
level,
60+
};
61+
}
62+
63+
if (first && typeof first === "object") {
64+
return {
65+
msg: toMessage(first as object),
66+
level,
67+
};
68+
}
69+
70+
return {
71+
msg: String(first ?? ""),
72+
level,
73+
};
74+
}
75+
76+
function push(stream: TestLogEntry[], level: number, ...args: unknown[]): void {
77+
stream.push(parseEntry(level, args));
78+
}
79+
80+
export function toEntry(level: number, args: unknown[]): TestLogEntry {
81+
return parseEntry(level, args);
82+
}
83+
84+
export function pushEntry(
85+
stream: TestLogEntry[],
86+
level: number,
87+
args: unknown[],
88+
): void {
89+
stream.push(parseEntry(level, args));
90+
}
91+
92+
export function* createCapturedLogger(stream: TestLogEntry[]) {
93+
const sink = {
94+
info: [],
95+
error: [],
96+
warn: [],
97+
debug: [],
98+
fatal: [],
99+
stdout: [],
100+
stderr: [],
101+
} as Record<string, TestLogEntry[]>;
102+
103+
yield* logger.around({
104+
*info(args, _next) {
105+
sink.info.push(...args.map((arg) => toEntry(30, [arg])));
106+
},
107+
*error(args, _next) {
108+
sink.error.push(...args.map((arg) => toEntry(50, [arg])));
109+
},
110+
*warn(args, _next) {
111+
sink.warn.push(...args.map((arg) => toEntry(40, [arg])));
112+
},
113+
*debug(args, _next) {
114+
sink.debug.push(...args.map((arg) => toEntry(20, [arg])));
115+
},
116+
*fatal(args, _next) {
117+
sink.fatal.push(...args.map((arg) => toEntry(60, [arg])));
118+
},
119+
*stdout(args, _next) {
120+
if (typeof args[0] === "string") {
121+
sink.stdout.push(...args.map((arg) => toEntry(30, [arg])));
122+
}
123+
},
124+
*stderr(args, _next) {
125+
if (typeof args[0] === "string") {
126+
sink.stderr.push(...args.map((arg) => toEntry(30, [arg])));
127+
}
128+
},
129+
});
130+
return sink;
131+
}
132+
133+
export function consecutive(
134+
actual: TestLogEntry[],
135+
expected: Array<Partial<TestLogEntry>>,
136+
matcher: (
137+
actualEntry: TestLogEntry,
138+
expectedEntry: Partial<TestLogEntry>,
139+
) => void = (actualEntry, expectedEntry) => {
140+
if (
141+
Array.isArray(expectedEntry.msg) &&
142+
typeof actualEntry.msg === "string"
143+
) {
144+
for (const part of expectedEntry.msg) {
145+
if (typeof part === "string") {
146+
expect(actualEntry.msg).toContain(part);
147+
}
148+
}
149+
const { msg: _msg, ...rest } = expectedEntry;
150+
expect(actualEntry).toMatchObject(rest);
151+
return;
152+
}
153+
154+
if (
155+
typeof expectedEntry.msg === "string" &&
156+
typeof actualEntry.msg === "string"
157+
) {
158+
expect(actualEntry.msg).toContain(expectedEntry.msg);
159+
const { msg: _msg, ...rest } = expectedEntry;
160+
expect(actualEntry).toMatchObject(rest);
161+
return;
162+
}
163+
164+
expect(actualEntry).toMatchObject(expectedEntry);
165+
},
166+
): void {
167+
if (expected.length === 0) return;
168+
169+
let cursor = 0;
170+
for (const expectedEntry of expected) {
171+
let found = false;
172+
for (let index = cursor; index < actual.length; index++) {
173+
try {
174+
matcher(actual[index], expectedEntry);
175+
cursor = index + 1;
176+
found = true;
177+
break;
178+
} catch {
179+
// keep scanning forward for this expected entry
180+
}
181+
}
182+
183+
if (!found && canSkipExpected(expectedEntry)) {
184+
continue;
185+
}
186+
187+
if (!found) {
188+
throw new Error(
189+
`expected log entry not found in order. expected=${JSON.stringify(expectedEntry)} actual=${JSON.stringify(actual)}`,
190+
);
191+
}
192+
}
193+
}
194+
195+
function canSkipExpected(expectedEntry: Partial<TestLogEntry>): boolean {
196+
if (
197+
expectedEntry.level !== 30 ||
198+
typeof expectedEntry.msg !== "string" ||
199+
!("command" in expectedEntry)
200+
) {
201+
return false;
202+
}
203+
204+
if (expectedEntry.msg === "completed") return false;
205+
if (expectedEntry.msg.includes("]:")) return false;
206+
if (expectedEntry.msg.startsWith("Checking if ")) return false;
207+
if (expectedEntry.msg.startsWith("dryRun >> ")) return false;
208+
return true;
209+
}

0 commit comments

Comments
 (0)