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

Commit

Permalink
Merge pull request #171 from getappmap/parse-jest-command
Browse files Browse the repository at this point in the history
feat: support additional command patterns for mocha and jest
  • Loading branch information
lachrist authored Jan 11, 2023
2 parents 6b69710 + 0133cfd commit 4ea08b3
Show file tree
Hide file tree
Showing 19 changed files with 460 additions and 193 deletions.
1 change: 1 addition & 0 deletions components/configuration-accessor/default/.ordering
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
escape.mjs
tokenize.mjs
package.mjs
mocha.mjs
jest.mjs
Expand Down
32 changes: 17 additions & 15 deletions components/configuration-accessor/default/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,23 @@ export const resolveConfigurationAutomatedRecorder = (configuration) => {
"cannot resolve recorder because command is missing",
InternalAppmapError,
);
const method =
configuration.command.tokens === null
? "doesSupportSource"
: "doesSupportTokens";
const input =
configuration.command.tokens === null
? configuration.command.source
: configuration.command.tokens;
const { name } = recorders.find(
(recorder) =>
(recorder.recursive === null ||
recorder.recursive ===
configuration["recursive-process-recording"]) &&
recorder[method](input),
);
const { name } = recorders.find((recorder) => {
if (
recorder.recursive === null ||
recorder.recursive === configuration["recursive-process-recording"]
) {
if (configuration.command.tokens === null) {
return recorder.doesSupportSource(
configuration.command.source,
configuration.command.shell,
);
} else {
return recorder.doesSupportTokens(configuration.command.tokens);
}
} else {
return false;
}
});
configuration = extendConfiguration(
configuration,
{
Expand Down
28 changes: 16 additions & 12 deletions components/configuration-accessor/default/jest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,39 @@ import { coalesce } from "../../util/index.mjs";
import { convertFileUrlToPath } from "../../path/index.mjs";
import { toAbsoluteUrl } from "../../url/index.mjs";
import { escapeNodeOption, escapeShell } from "./escape.mjs";
import { generateParseSource, generateSplitTokens } from "./package.mjs";

const { canParseSource, parseSource } = generateParseSource("jest");
const { canSplitTokens, splitTokens } = generateSplitTokens("jest");
import {
sniffSource,
sniffTokens,
parseSource,
splitTokens,
} from "./package.mjs";

export const name = "jest";
export const recursive = null;

export const doesSupportSource = canParseSource;
export const doesSupportTokens = canSplitTokens;
export const doesSupportSource = (source, shell) =>
sniffSource(source, "jest", shell);

export const doesSupportTokens = (tokens) => sniffTokens(tokens, "jest");

export const hookCommandSource = (source, shell, base) => {
const groups = parseSource(source);
const groups = parseSource(source, shell);
return [
`${groups.before} --runInBand --setupFilesAfterEnv ${escapeShell(
`${groups.exec} --runInBand --setupFilesAfterEnv ${escapeShell(
shell,
convertFileUrlToPath(toAbsoluteUrl("lib/node/recorder-jest.mjs", base)),
)}${groups.after}`,
)} ${groups.argv}`,
];
};

export const hookCommandTokens = (tokens, base) => {
const { before, after } = splitTokens(tokens);
const { exec, argv } = splitTokens(tokens);
return [
...before,
...exec,
"--runInBand",
"--setupFilesAfterEnv",
convertFileUrlToPath(toAbsoluteUrl("lib/node/recorder-jest.mjs", base)),
...after,
...argv,
];
};

Expand Down
2 changes: 1 addition & 1 deletion components/configuration-accessor/default/jest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const loader_url = "file:///A:/base/lib/node/loader-standalone.mjs";
//////////////////

// source //
assertEqual(doesSupportSource("jest --argv"), true);
assertEqual(doesSupportSource("jest --argv", "/bin/sh"), true);
assertDeepEqual(hookCommandSource("jest --argv", "/bin/sh", base), [
`jest --runInBand --setupFilesAfterEnv ${recorder_path.replace(
/\\/gu,
Expand Down
25 changes: 14 additions & 11 deletions components/configuration-accessor/default/mocha.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,38 @@ import { coalesce } from "../../util/index.mjs";
import { convertFileUrlToPath } from "../../path/index.mjs";
import { toAbsoluteUrl } from "../../url/index.mjs";
import { escapeShell, escapeNodeOption } from "./escape.mjs";
import { generateParseSource, generateSplitTokens } from "./package.mjs";

const { canParseSource, parseSource } = generateParseSource("mocha");
const { canSplitTokens, splitTokens } = generateSplitTokens("mocha");
import {
sniffSource,
sniffTokens,
parseSource,
splitTokens,
} from "./package.mjs";

export const name = "mocha";
export const recursive = null;

export const doesSupportSource = canParseSource;
export const doesSupportSource = (source, shell) =>
sniffSource(source, "mocha", shell);

export const doesSupportTokens = canSplitTokens;
export const doesSupportTokens = (tokens) => sniffTokens(tokens, "mocha");

export const hookCommandSource = (source, shell, base) => {
const groups = parseSource(source);
return [
`${groups.before} --require ${escapeShell(
`${groups.exec} --require ${escapeShell(
shell,
convertFileUrlToPath(toAbsoluteUrl("lib/node/recorder-mocha.mjs", base)),
)}${groups.after}`,
)} ${groups.argv}`,
];
};

export const hookCommandTokens = (tokens, base) => {
const { before, after } = splitTokens(tokens);
const { exec, argv } = splitTokens(tokens);
return [
...before,
...exec,
"--require",
convertFileUrlToPath(toAbsoluteUrl("lib/node/recorder-mocha.mjs", base)),
...after,
...argv,
];
};

Expand Down
2 changes: 1 addition & 1 deletion components/configuration-accessor/default/mocha.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const loader_url = "file:///A:/base/lib/node/loader-standalone.mjs";
//////////////////

// source //
assertEqual(doesSupportSource("mocha --argv"), true);
assertEqual(doesSupportSource("mocha --argv", "/bin/sh"), true);
assertDeepEqual(hookCommandSource("mocha --argv", "/bin/sh", base), [
`mocha --require ${recorder_path.replace(/\\/gu, "\\\\")} --argv`,
]);
Expand Down
18 changes: 9 additions & 9 deletions components/configuration-accessor/default/node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { toAbsoluteUrl } from "../../url/index.mjs";
import { ExternalAppmapError } from "../../error/index.mjs";
import { escapeShell } from "./escape.mjs";

const regexp = /^(?<before>\s*\S*node(.[a-zA-Z]+)?)(?<after>($|\s[\s\S]*$))$/u;
const regexp = /^(?<exec>\s*\S*node(.[a-zA-Z]+)?)(?<argv>($|\s[\s\S]*$))$/u;

const doesSupportSource = (command) => regexp.test(command);
const doesSupportSource = (source, _shell) => regexp.test(source);

const doesSupportTokens = (tokens) =>
tokens.length > 0 && tokens[0].startsWith("node");
Expand All @@ -22,8 +22,8 @@ const splitNodeCommand = (tokens) => {
ExternalAppmapError,
);
return {
before: tokens.slice(0, 1),
after: tokens.slice(1),
exec: tokens.slice(0, 1),
argv: tokens.slice(1),
};
};

Expand All @@ -49,19 +49,19 @@ export const generateNodeRecorder = (recorder) => ({
hookCommandSource: (source, shell, base) => {
const groups = parseNodeCommand(source);
return [
`${groups.before} --experimental-loader ${escapeShell(
`${groups.exec} --experimental-loader ${escapeShell(
shell,
toAbsoluteUrl(`lib/node/recorder-${recorder}.mjs`, base),
)}${groups.after}`,
)}${groups.argv}`,
];
},
hookCommandTokens: (tokens, base) => {
const { before, after } = splitNodeCommand(tokens);
const { exec, argv } = splitNodeCommand(tokens);
return [
...before,
...exec,
"--experimental-loader",
toAbsoluteUrl(`lib/node/recorder-${recorder}.mjs`, base),
...after,
...argv,
];
},
hookEnvironment: (env, _base) => env,
Expand Down
154 changes: 112 additions & 42 deletions components/configuration-accessor/default/package.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { InternalAppmapError } from "../../error/index.mjs";
import { assert } from "../../util/index.mjs";

const { RegExp } = globalThis;
import { logErrorWhen } from "../../log/index.mjs";

import { ExternalAppmapError } from "../../error/index.mjs";

import { tokenizeShell, tokenizeCmdShell } from "./tokenize.mjs";

const isPrefixArray = (prefix, array) => {
const { length } = prefix;
Expand All @@ -16,51 +20,117 @@ const isPrefixArray = (prefix, array) => {
}
};

export const generateParseSource = (name) => {
const regexps = [
new RegExp(`^(?<before>${name})(?<after>($|\\s[\\s\\S]*$))`, "u"),
new RegExp(`^(?<before>npx\\s+${name})(?<after>($|\\s[\\s\\S]*$))`, "u"),
new RegExp(
`^(?<before>npm\\s+exec\\s+mocha)(?<after>($|\\s[\\s\\S]*$))`,
"u",
const findScriptIndex = (tokens, index) => {
if (index < tokens.length) {
const token = tokens[index];
// In the node cli, `-` indicates that the script
// is read from the stdin. In that case, we should
// not return any index. For instance:
// `node - foo.js`
if (token === "-") {
return null;
// In the CLI of node, npm and npx: `--` indicates
// the the script argument is following.
} else if (token === "--") {
return index + 1 < tokens.length ? index + 1 : null;
// We only support named argument of the form
// `--foo=bar` and not `--foo bar`.
} else if (token.startsWith("-")) {
return findScriptIndex(tokens, index + 1);
} else {
return index;
}
} else {
return null;
}
};

const executables = [
["node"],
["npm", "exec"],
["npm", "x"],
["npx"],
["npm.cmd", "exec"],
["npm.cmd", "x"],
["npx.cmd"],
[],
];

export const splitTokens = (tokens) => {
const executable = executables.find((executable) =>
isPrefixArray(executable, tokens),
);
const positional = findScriptIndex(tokens, executable.length);
assert(
!logErrorWhen(
positional === null,
"could not parse and hook command because of missing positional argument, got %j",
tokens,
),
];
"could not parse and hook command",
ExternalAppmapError,
);
return {
canParseSource: (source) => regexps.some((regexp) => regexp.test(source)),
parseSource: (source) => {
for (const regexp of regexps) {
const result = regexp.exec(source);
if (result !== null) {
return result.groups;
}
}
throw new InternalAppmapError("could not parse command source");
},
__proto__: null,
exec: tokens.slice(0, positional + 1),
argv: tokens.slice(positional + 1),
};
};

export const sniffTokens = (tokens, name) => {
const executable = executables.find((executable) =>
isPrefixArray(executable, tokens),
);
const positional = findScriptIndex(tokens, executable.length);
return positional !== null && tokens[positional].includes(name);
};

const chopSetEnv = (tokens) => {
let index = 0;
while (
index < tokens.length &&
!tokens[index].startsWith("-") &&
tokens[index].includes("=")
) {
index += 1;
}
return {
head: tokens.slice(0, index),
body: tokens.slice(index),
};
};

export const generateSplitTokens = (name) => {
const prefixes = [
[name],
["npx", name],
["npx.cmd", name],
["npm", "exec", name],
["npm.cmd", "exec", name],
];
const chopNothing = (tokens) => ({
head: [],
body: tokens,
});

const isCmdShell = (shell) => shell === "cmd" || shell === "cmd.exe";

const isPowerShell = (shell) =>
shell === "powershell" || shell === "powershell.exe";

const chop = (tokens, shell) =>
isCmdShell(shell) || isPowerShell(shell)
? chopNothing(tokens)
: chopSetEnv(tokens);

const tokenize = (source, shell) =>
isCmdShell(shell) ? tokenizeCmdShell(source) : tokenizeShell(source);

export const parseSource = (source, shell) => {
const tokens = tokenize(source, shell);
const { head, body } = chop(tokens, shell);
const { exec, argv } = splitTokens(body);
return {
canSplitTokens: (tokens) =>
prefixes.some((prefix) => isPrefixArray(prefix, tokens)),
splitTokens: (tokens) => {
for (const prefix of prefixes) {
if (isPrefixArray(prefix, tokens)) {
return {
__proto__: null,
before: prefix,
after: tokens.slice(prefix.length),
};
}
}
throw new InternalAppmapError("could not split command tokens");
},
__proto__: null,
exec: [...head, ...exec].join(" "),
argv: argv.join(" "),
};
};

export const sniffSource = (source, name, shell) => {
const tokens = tokenize(source, shell);
const { body } = chop(tokens, shell);
return sniffTokens(body, name);
};
Loading

0 comments on commit 4ea08b3

Please sign in to comment.