Skip to content
This repository was archived by the owner on Mar 15, 2024. It is now read-only.

Commit 1052eb9

Browse files
dividedmindbrikelly
authored andcommitted
feat: Record test failure details in mocha
Fixes #231
1 parent f5b5567 commit 1052eb9

File tree

8 files changed

+347
-222
lines changed

8 files changed

+347
-222
lines changed

components/recorder-cli/mocha/index.mjs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import process from "node:process";
22
import { hooks } from "../../../lib/node/mocha-hook.mjs";
3-
import { ExternalAppmapError } from "../../error/index.mjs";
3+
import {
4+
ExternalAppmapError,
5+
parseExceptionStack,
6+
} from "../../error/index.mjs";
47
import { logInfo, logErrorWhen } from "../../log/index.mjs";
58
import { getUuid } from "../../uuid/index.mjs";
69
import { extendConfiguration } from "../../configuration/index.mjs";
@@ -26,8 +29,24 @@ import {
2629
closeSocket,
2730
addSocketListener,
2831
} from "../../socket/index.mjs";
32+
import { stringifyLocation } from "../../location/index.mjs";
33+
34+
const { undefined, parseInt, Boolean, Promise } = globalThis;
2935

30-
const { undefined, parseInt, Promise } = globalThis;
36+
const buildTestFailure = (error) => {
37+
if (!error?.name) {
38+
return undefined;
39+
}
40+
const message = [error.name, error.message].filter(Boolean).join(": ");
41+
const frame = parseExceptionStack(error)?.at(0);
42+
if (!frame) {
43+
return { message };
44+
}
45+
return {
46+
message,
47+
location: stringifyLocation({ position: frame, url: frame.fileName }),
48+
};
49+
};
3150

3251
// Accessing mocha version via the prototype is not documented but it seems stable enough.
3352
// Added in https://github.com/mochajs/mocha/pull/3535
@@ -140,9 +159,11 @@ export const recordAsync = (configuration) => {
140159
"No running mocha test case",
141160
ExternalAppmapError,
142161
);
162+
const passed = context.currentTest.state === "passed";
143163
recordStopTrack(frontend, running.track, {
144164
type: "test",
145-
passed: context.currentTest.state === "passed",
165+
passed,
166+
failure: passed ? undefined : buildTestFailure(context.currentTest.err),
146167
});
147168
flush();
148169
running = null;

components/recorder-cli/mocha/index.test.mjs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "../../configuration/index.mjs";
1111
import { recordAsync } from "./index.mjs";
1212

13-
const { setImmediate } = globalThis;
13+
const { Error, setImmediate, undefined } = globalThis;
1414

1515
const getLastMockSocket = readGlobal("GET_LAST_MOCK_SOCKET");
1616
const receiveMockSocket = readGlobal("RECEIVE_MOCK_SOCKET");
@@ -45,18 +45,48 @@ setImmediate(() => {
4545
receiveMockSocket(getLastMockSocket(), "0");
4646
});
4747

48-
await beforeEach.call({
49-
currentTest: {
50-
parent: {
51-
fullTitle: () => "full-title-1",
48+
const startTest = () =>
49+
beforeEach.call({
50+
currentTest: {
51+
parent: {
52+
fullTitle: () => "full-title-1",
53+
},
5254
},
55+
});
56+
57+
await startTest();
58+
59+
await afterEach.call({
60+
currentTest: {
61+
state: "passed",
5362
},
5463
});
5564

65+
await startTest();
66+
5667
await afterEach.call({
5768
currentTest: {
58-
state: "passed",
69+
state: "failed",
70+
err: new Error("test error"),
5971
},
6072
});
6173

6274
await afterAll();
75+
76+
const testFailure = async (error) => {
77+
await startTest();
78+
return afterEach.call({
79+
currentTest: {
80+
state: "failed",
81+
err: error,
82+
},
83+
});
84+
};
85+
86+
await testFailure(undefined);
87+
await testFailure({ name: "TestError", message: "test error" });
88+
await testFailure({
89+
name: "TestError",
90+
message: "test error",
91+
stack: "TestError: test error\n at /app/foo-bar.js:4:6",
92+
});

components/trace/appmap/index.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
logInfo,
1010
logInfoWhen,
1111
} from "../../log/index.mjs";
12+
import { toRelativeUrl } from "../../url/index.mjs";
1213
import { validateAppmap } from "../../validate-appmap/index.mjs";
1314
import { parseLocation } from "../../location/index.mjs";
1415
import {
@@ -22,11 +23,13 @@ import { orderEventArray } from "./ordering/index.mjs";
2223
import { getOutputUrl } from "./output.mjs";
2324

2425
const {
26+
Boolean,
2527
Map,
2628
Array: { from: toArray },
2729
String,
2830
Math: { round },
2931
RangeError,
32+
URL,
3033
} = globalThis;
3134

3235
const VERSION = "1.8.0";
@@ -181,6 +184,18 @@ const printApplyEventDistribution = (events, codebase) => {
181184
.join("");
182185
};
183186

187+
const makeFailure = ({ message, location }, { base }) => {
188+
if (!location) {
189+
return { message };
190+
}
191+
const {
192+
url,
193+
position: { line },
194+
} = parseLocation(location);
195+
const relative = toRelativeUrl(new URL(url, base), base);
196+
return { message, location: [relative, line].filter(Boolean).join(":") };
197+
};
198+
184199
export const compileTrace = (configuration, files, messages, termination) => {
185200
logDebug("Trace: %j", { configuration, files, messages, termination });
186201
const errors = [];
@@ -290,12 +305,18 @@ export const compileTrace = (configuration, files, messages, termination) => {
290305
}
291306
}
292307
/* c8 ignore stop */
308+
293309
const appmap = {
294310
version: VERSION,
295311
metadata: compileMetadata(configuration, errors, termination),
296312
classMap: exportClassmap(codebase),
297313
events: digested_events,
298314
};
315+
316+
if (termination.failure) {
317+
appmap.metadata.test_failure = makeFailure(termination.failure, codebase);
318+
}
319+
299320
if (configuration.validate.appmap) {
300321
validateAppmap(appmap);
301322
}

0 commit comments

Comments
 (0)