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

Commit

Permalink
feat: Record test failure details in mocha
Browse files Browse the repository at this point in the history
Fixes #231
  • Loading branch information
dividedmind committed Aug 3, 2023
1 parent 25bb77d commit 5400c82
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 236 deletions.
50 changes: 35 additions & 15 deletions components/location/default/index.mjs
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
import { InternalAppmapError } from "../../error/index.mjs";
import { assert } from "../../util/index.mjs";

const { String, parseInt, undefined } = globalThis;
const { parseInt, undefined } = globalThis;

const regexp = /^([A-Za-z0-9+/=]+\|)?([\s\S]+):([0-9]+):([0-9]+)$/u;
const regexp = /^(?:([A-Za-z0-9+/=]+)\|)?(.+)$/u;

export const stringifyLocation = ({
url,
hash,
position: { line, column },
}) => {
const position = [line, column].filter((x) => x !== undefined).join(":");
if (hash === null) {
return `${url}:${String(line)}:${String(column)}`;
return `${url}:${position}`;
} else {
return `${hash}|${url}:${String(line)}:${String(column)}`;
return `${hash}|${url}:${position}`;
}
};

const popInt = (arr) => {
const last = arr.pop();
if (last.match(/\d+/u)) {
return parseInt(last);
}
arr.push(last);
return undefined;
};

export const parseLocation = (string) => {
const parts = regexp.exec(string);
assert(parts !== null, "invalid location format", InternalAppmapError);
const [, hash, rest] = regexp.exec(string);
assert(
rest !== null,
`invalid location format: ${string}`,
InternalAppmapError,
);

const parts = rest.split(":");
const loc2 = popInt(parts);
const loc1 = popInt(parts);
let line, column;
if (loc1 !== undefined) {
line = parseInt(loc1);
column = parseInt(loc2);
} else if (loc2 !== undefined) {
line = parseInt(loc2);
}

return {
hash:
parts[1] === undefined
? null
: parts[1].substring(0, parts[1].length - 1),
url: parts[2],
position: {
line: parseInt(parts[3]),
column: parseInt(parts[4]),
},
hash: hash ?? null,
url: parts.join(":"),
position: { line, column },
};
};
8 changes: 8 additions & 0 deletions components/location/default/index.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { assertDeepEqual } from "../../__fixture__.mjs";
import { stringifyLocation, parseLocation } from "./index.mjs";

const { undefined } = globalThis;

const test = (location) => {
assertDeepEqual(parseLocation(stringifyLocation(location)), location);
};
Expand All @@ -16,3 +18,9 @@ test({
url: "protocol://host/path",
position: { line: 123, column: 456 },
});

test({
hash: null,
url: "protocol://host/path",
position: { line: 123, column: undefined },
});
29 changes: 26 additions & 3 deletions components/recorder-cli/mocha/index.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { isAbsolute } from "node:path";
import process from "node:process";
import { hooks } from "../../../lib/node/mocha-hook.mjs";
import { ExternalAppmapError } from "../../error/index.mjs";
import {
ExternalAppmapError,
parseExceptionStack,
} from "../../error/index.mjs";
import { logInfo, logErrorWhen } from "../../log/index.mjs";
import { getUuid } from "../../uuid/index.mjs";
import { extendConfiguration } from "../../configuration/index.mjs";
Expand All @@ -27,7 +31,24 @@ import {
addSocketListener,
} from "../../socket/index.mjs";

const { undefined, parseInt, Promise } = globalThis;
const { undefined, parseInt, Boolean, Promise } = globalThis;

function buildTestFailure(error) {
if (!error?.name) {
return undefined;
}
const message = [error.name, error.message].filter(Boolean).join(": ");
const frame = parseExceptionStack(error)?.find(({ fileName }) =>
isAbsolute(fileName),
);
if (!frame) {
return { message };
}
return {
message,
location: [frame.fileName, frame.line].filter(Boolean).join(":"),
};
}

// Accessing mocha version via the prototype is not documented but it seems stable enough.
// Added in https://github.com/mochajs/mocha/pull/3535
Expand Down Expand Up @@ -140,9 +161,11 @@ export const recordAsync = (configuration) => {
"No running mocha test case",
ExternalAppmapError,
);
const passed = context.currentTest.state === "passed";
recordStopTrack(frontend, running.track, {
type: "test",
passed: context.currentTest.state === "passed",
passed,
failure: passed ? undefined : buildTestFailure(context.currentTest.err),
});
flush();
running = null;
Expand Down
43 changes: 37 additions & 6 deletions components/recorder-cli/mocha/index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "../../configuration/index.mjs";
import { recordAsync } from "./index.mjs";

const { setImmediate } = globalThis;
const { Error, setImmediate, undefined } = globalThis;

const getLastMockSocket = readGlobal("GET_LAST_MOCK_SOCKET");
const receiveMockSocket = readGlobal("RECEIVE_MOCK_SOCKET");
Expand Down Expand Up @@ -45,18 +45,49 @@ setImmediate(() => {
receiveMockSocket(getLastMockSocket(), "0");
});

await beforeEach.call({
currentTest: {
parent: {
fullTitle: () => "full-title-1",
function startTest() {
return beforeEach.call({
currentTest: {
parent: {
fullTitle: () => "full-title-1",
},
},
});
}

await startTest();

await afterEach.call({
currentTest: {
state: "passed",
},
});

await startTest();

await afterEach.call({
currentTest: {
state: "passed",
state: "failed",
err: new Error("test error"),
},
});

await afterAll();

async function testFailure(error) {
await startTest();
return afterEach.call({
currentTest: {
state: "failed",
err: error,
},
});
}

await testFailure(undefined);
await testFailure({ name: "TestError", message: "test error" });
await testFailure({
name: "TestError",
message: "test error",
stack: "TestError: test error\n at /app/foo-bar.js:4:6",
});
21 changes: 21 additions & 0 deletions components/trace/appmap/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
logInfo,
logInfoWhen,
} from "../../log/index.mjs";
import { toRelativeUrl } from "../../url/index.mjs";
import { validateAppmap } from "../../validate-appmap/index.mjs";
import { parseLocation } from "../../location/index.mjs";
import {
Expand All @@ -22,11 +23,13 @@ import { orderEventArray } from "./ordering/index.mjs";
import { getOutputUrl } from "./output.mjs";

const {
Boolean,
Map,
Array: { from: toArray },
String,
Math: { round },
RangeError,
URL,
} = globalThis;

const VERSION = "1.8.0";
Expand Down Expand Up @@ -181,6 +184,18 @@ const printApplyEventDistribution = (events, codebase) => {
.join("");
};

function makeFailure({ message, location }, { base }) {
if (!location) {
return { message };
}
const {
url,
position: { line },
} = parseLocation(location);
const relative = toRelativeUrl(new URL(url, base), base);
return { message, location: [relative, line].filter(Boolean).join(":") };
}

export const compileTrace = (configuration, files, messages, termination) => {
logDebug("Trace: %j", { configuration, files, messages, termination });
const errors = [];
Expand Down Expand Up @@ -290,12 +305,18 @@ export const compileTrace = (configuration, files, messages, termination) => {
}
}
/* c8 ignore stop */

const appmap = {
version: VERSION,
metadata: compileMetadata(configuration, errors, termination),
classMap: exportClassmap(codebase),
events: digested_events,
};

if (termination.failure) {
appmap.metadata.test_failure = makeFailure(termination.failure, codebase);
}

if (configuration.validate.appmap) {
validateAppmap(appmap);
}
Expand Down
Loading

0 comments on commit 5400c82

Please sign in to comment.