refactor: `load` uses dependency injection (#1123)
This commit refactors the `sourcecred load` CLI command so that it uses dependency injection, much like the testing setup #1110. This makes it feasible to test "surface logic" of how the CLI parses flags and transforms them into data separately from the "piping logic" of invoking the right API calls using that data. This is motivated by the fact that I have other pulls on the way that modify the `load` command (e.g. #1115) and testing them within the current framework is onerous. Test plan: This is a pure refactoring commit, which substantially re-writes the unit tests. The new unit tests pass (`yarn test --full` is happy). Note that `yarn test -full` also includes a sharness test that does an E2E usage of `sourcecred load` (see sharness/test_load_example_github.t), so we may be confident that the command still works as intended.
This commit is contained in:
parent
13a90675a8
commit
320a69759e
170
src/cli/load.js
170
src/cli/load.js
|
@ -4,6 +4,8 @@
|
|||
import mkdirp from "mkdirp";
|
||||
import path from "path";
|
||||
|
||||
import * as NullUtil from "../util/null";
|
||||
|
||||
import * as RepoIdRegistry from "../core/repoIdRegistry";
|
||||
import {repoIdToString, stringToRepoId, type RepoId} from "../core/repoId";
|
||||
import dedent from "../util/dedent";
|
||||
|
@ -75,66 +77,94 @@ function die(std, message) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
const load: Command = async (args, std) => {
|
||||
const repoIds = [];
|
||||
let explicitOutput: RepoId | null = null;
|
||||
let plugin: Common.PluginName | null = null;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "--help": {
|
||||
usage(std.out);
|
||||
return 0;
|
||||
}
|
||||
case "--output": {
|
||||
if (explicitOutput != null)
|
||||
return die(std, "'--output' given multiple times");
|
||||
if (++i >= args.length)
|
||||
return die(std, "'--output' given without value");
|
||||
explicitOutput = stringToRepoId(args[i]);
|
||||
break;
|
||||
}
|
||||
case "--plugin": {
|
||||
if (plugin != null) return die(std, "'--plugin' given multiple times");
|
||||
if (++i >= args.length)
|
||||
return die(std, "'--plugin' given without value");
|
||||
const arg = args[i];
|
||||
if (arg !== "git" && arg !== "github")
|
||||
return die(std, "unknown plugin: " + JSON.stringify(arg));
|
||||
plugin = arg;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Should be a repository.
|
||||
repoIds.push(stringToRepoId(args[i]));
|
||||
break;
|
||||
export type LoadOptions = {|
|
||||
+output: RepoId,
|
||||
+repoIds: $ReadOnlyArray<RepoId>,
|
||||
|};
|
||||
|
||||
export function makeLoadCommand(
|
||||
loadIndividualPlugin: (Common.PluginName, LoadOptions) => Promise<void>,
|
||||
loadDefaultPlugins: (LoadOptions) => Promise<void>
|
||||
): Command {
|
||||
return async function load(args, std) {
|
||||
if (Common.githubToken() == null) {
|
||||
// TODO(#638): This check should be abstracted so that plugins can
|
||||
// specify their argument dependencies and get nicely formatted
|
||||
// errors.
|
||||
// For simplicity, for now while we are always using GitHub, we just
|
||||
// check for the GitHub token upfront for all load commands.
|
||||
return die(std, "no GitHub token specified");
|
||||
}
|
||||
|
||||
const repoIds: RepoId[] = [];
|
||||
let explicitOutput: RepoId | null = null;
|
||||
let plugin: Common.PluginName | null = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "--help": {
|
||||
usage(std.out);
|
||||
return 0;
|
||||
}
|
||||
case "--output": {
|
||||
if (explicitOutput != null)
|
||||
return die(std, "'--output' given multiple times");
|
||||
if (++i >= args.length)
|
||||
return die(std, "'--output' given without value");
|
||||
explicitOutput = stringToRepoId(args[i]);
|
||||
break;
|
||||
}
|
||||
case "--plugin": {
|
||||
if (plugin != null)
|
||||
return die(std, "'--plugin' given multiple times");
|
||||
if (++i >= args.length)
|
||||
return die(std, "'--plugin' given without value");
|
||||
const arg = args[i];
|
||||
if (arg !== "git" && arg !== "github")
|
||||
return die(std, "unknown plugin: " + JSON.stringify(arg));
|
||||
plugin = arg;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Should be a repository.
|
||||
repoIds.push(stringToRepoId(args[i]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output: RepoId;
|
||||
if (explicitOutput != null) {
|
||||
output = explicitOutput;
|
||||
} else if (repoIds.length === 1) {
|
||||
output = repoIds[0];
|
||||
} else {
|
||||
return die(std, "output repository not specified");
|
||||
}
|
||||
let output: RepoId;
|
||||
if (explicitOutput != null) {
|
||||
output = explicitOutput;
|
||||
} else if (repoIds.length === 1) {
|
||||
output = repoIds[0];
|
||||
} else {
|
||||
return die(std, "output repository not specified");
|
||||
}
|
||||
|
||||
if (plugin == null) {
|
||||
return loadDefaultPlugins({std, output, repoIds});
|
||||
} else {
|
||||
return loadPlugin({std, output, repoIds, plugin});
|
||||
}
|
||||
};
|
||||
const options: LoadOptions = {output, repoIds: repoIds};
|
||||
|
||||
const loadDefaultPlugins = async ({std, output, repoIds}) => {
|
||||
if (Common.githubToken() == null) {
|
||||
// TODO(#638): This check should be abstracted so that plugins can
|
||||
// specify their argument dependencies and get nicely formatted
|
||||
// errors.
|
||||
return die(std, "no GitHub token specified");
|
||||
}
|
||||
if (plugin == null) {
|
||||
try {
|
||||
await loadDefaultPlugins(options);
|
||||
return 0;
|
||||
} catch (e) {
|
||||
std.err(e.message);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await loadIndividualPlugin(plugin, options);
|
||||
return 0;
|
||||
} catch (e) {
|
||||
std.err(e.message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loadDefaultPlugins = async (options: LoadOptions) => {
|
||||
const tasks = [
|
||||
...Common.defaultPlugins().map((pluginName) => ({
|
||||
id: `load-${pluginName}`,
|
||||
|
@ -143,9 +173,9 @@ const loadDefaultPlugins = async ({std, output, repoIds}) => {
|
|||
"--max_old_space_size=8192",
|
||||
process.argv[1],
|
||||
"load",
|
||||
...repoIds.map((repoId) => repoIdToString(repoId)),
|
||||
...options.repoIds.map((repoId) => repoIdToString(repoId)),
|
||||
"--output",
|
||||
repoIdToString(output),
|
||||
repoIdToString(options.output),
|
||||
"--plugin",
|
||||
pluginName,
|
||||
],
|
||||
|
@ -155,12 +185,18 @@ const loadDefaultPlugins = async ({std, output, repoIds}) => {
|
|||
|
||||
const {success} = await execDependencyGraph(tasks, {taskPassLabel: "DONE"});
|
||||
if (success) {
|
||||
addToRepoIdRegistry(output);
|
||||
addToRepoIdRegistry(options.output);
|
||||
return;
|
||||
} else {
|
||||
throw new Error("Load tasks failed.");
|
||||
}
|
||||
return success ? 0 : 1;
|
||||
};
|
||||
|
||||
const loadPlugin = async ({std, output, repoIds, plugin}) => {
|
||||
export const loadIndividualPlugin = async (
|
||||
plugin: Common.PluginName,
|
||||
options: LoadOptions
|
||||
) => {
|
||||
const {output, repoIds} = options;
|
||||
function scopedDirectory(key) {
|
||||
const directory = path.join(
|
||||
Common.sourcecredDirectory(),
|
||||
|
@ -175,24 +211,18 @@ const loadPlugin = async ({std, output, repoIds, plugin}) => {
|
|||
const cacheDirectory = scopedDirectory("cache");
|
||||
switch (plugin) {
|
||||
case "github": {
|
||||
const token = Common.githubToken();
|
||||
if (token == null) {
|
||||
// TODO(#638): This check should be abstracted so that plugins
|
||||
// can specify their argument dependencies and get nicely
|
||||
// formatted errors.
|
||||
return die(std, "no GitHub token specified");
|
||||
}
|
||||
const token = NullUtil.get(Common.githubToken());
|
||||
await loadGithubData({token, repoIds, outputDirectory, cacheDirectory});
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
case "git":
|
||||
await loadGitData({repoIds, outputDirectory, cacheDirectory});
|
||||
return 0;
|
||||
return;
|
||||
// Unlike the previous check, which was validating user input and
|
||||
// was reachable, this really should not occur.
|
||||
// istanbul ignore next
|
||||
default:
|
||||
return die(std, "unknown plugin: " + JSON.stringify((plugin: empty)));
|
||||
throw new Error("unknown plugin: " + JSON.stringify((plugin: empty)));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -214,4 +244,6 @@ export const help: Command = async (args, std) => {
|
|||
}
|
||||
};
|
||||
|
||||
const load = makeLoadCommand(loadIndividualPlugin, loadDefaultPlugins);
|
||||
|
||||
export default load;
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
// @flow
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import tmp from "tmp";
|
||||
|
||||
import {run} from "./testUtil";
|
||||
import load, {help} from "./load";
|
||||
import {
|
||||
makeLoadCommand,
|
||||
loadDefaultPlugins,
|
||||
loadIndividualPlugin,
|
||||
help,
|
||||
} from "./load";
|
||||
|
||||
import * as RepoIdRegistry from "../core/repoIdRegistry";
|
||||
import {stringToRepoId} from "../core/repoId";
|
||||
import {makeRepoId} from "../core/repoId";
|
||||
|
||||
jest.mock("../tools/execDependencyGraph", () => jest.fn());
|
||||
jest.mock("../plugins/github/loadGithubData", () => ({
|
||||
|
@ -63,9 +67,20 @@ describe("cli/load", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("'load' command", () => {
|
||||
describe("'load' command wrapper", () => {
|
||||
function setup() {
|
||||
const loadIndividualPlugin: any = jest.fn();
|
||||
const loadDefaultPlugins: any = jest.fn();
|
||||
const loadCommand = makeLoadCommand(
|
||||
loadIndividualPlugin,
|
||||
loadDefaultPlugins
|
||||
);
|
||||
return {loadIndividualPlugin, loadDefaultPlugins, loadCommand};
|
||||
}
|
||||
|
||||
it("prints usage with '--help'", async () => {
|
||||
expect(await run(load, ["--help"])).toEqual({
|
||||
const {loadCommand} = setup();
|
||||
expect(await run(loadCommand, ["--help"])).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: expect.arrayContaining([
|
||||
expect.stringMatching(/^usage: sourcecred load/),
|
||||
|
@ -74,393 +89,435 @@ describe("cli/load", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("for multiple repositories", () => {
|
||||
it("fails when no output is specified for two repoIds", async () => {
|
||||
expect(
|
||||
await run(load, ["foo/bar", "foo/baz", "--plugin", "git"])
|
||||
).toEqual({
|
||||
it("calls loadDefaultPlugins if plugin not specified", async () => {
|
||||
const {loadCommand, loadDefaultPlugins} = setup();
|
||||
const invocation = run(loadCommand, ["foo/bar"]);
|
||||
expect(await invocation).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
const expectedOptions = {repoIds: [repoId], output: repoId};
|
||||
expect(loadDefaultPlugins).toHaveBeenCalledWith(expectedOptions);
|
||||
});
|
||||
|
||||
it("calls loadIndividualPlugin if plugin explicitly specified", async () => {
|
||||
const {loadCommand, loadIndividualPlugin} = setup();
|
||||
const invocation = run(loadCommand, ["foo/bar", "--plugin", "git"]);
|
||||
expect(await invocation).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
const expectedOptions = {repoIds: [repoId], output: repoId};
|
||||
expect(loadIndividualPlugin).toHaveBeenCalledWith("git", expectedOptions);
|
||||
});
|
||||
|
||||
describe("errors if", () => {
|
||||
async function expectFailure({args, message}) {
|
||||
const {loadCommand, loadIndividualPlugin, loadDefaultPlugins} = setup();
|
||||
expect(await run(loadCommand, args)).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
stderr: message,
|
||||
});
|
||||
expect(loadIndividualPlugin).not.toHaveBeenCalled();
|
||||
expect(loadDefaultPlugins).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
it("no repos provided, and no output repository", async () => {
|
||||
await expectFailure({
|
||||
args: [],
|
||||
message: [
|
||||
"fatal: output repository not specified",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when no output is specified for zero repoIds", async () => {
|
||||
expect(await run(load, ["--plugin", "git"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
|
||||
it("multiple repos provided without output repository", async () => {
|
||||
await expectFailure({
|
||||
args: ["foo/bar", "zoink/zod"],
|
||||
message: [
|
||||
"fatal: output repository not specified",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when '--output' is given without a value", async () => {
|
||||
expect(await run(load, ["foo/bar", "--output"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--output' given without value",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when the same '--output' is given multiple times", async () => {
|
||||
expect(
|
||||
await run(load, [
|
||||
"foo/bar",
|
||||
"--output",
|
||||
"foo/baz",
|
||||
"--output",
|
||||
"foo/baz",
|
||||
])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--output' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when multiple '--output's are given", async () => {
|
||||
expect(
|
||||
await run(load, [
|
||||
"foo/bar",
|
||||
"--output",
|
||||
"foo/baz",
|
||||
"--output",
|
||||
"foo/quux",
|
||||
])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--output' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when loading single-plugin data", () => {
|
||||
it("fails for an unknown plugin", async () => {
|
||||
expect(await run(load, ["foo/bar", "--plugin", "wat"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
'fatal: unknown plugin: "wat"',
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when '--plugin' is given without a value", async () => {
|
||||
expect(await run(load, ["foo/bar", "--plugin"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--plugin' given without value",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when the same plugin is specified multiple times", async () => {
|
||||
expect(
|
||||
await run(load, ["foo/bar", "--plugin", "git", "--plugin", "git"])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--plugin' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
it("fails when multiple plugins are specified", async () => {
|
||||
expect(
|
||||
await run(load, ["foo/bar", "--plugin", "git", "--plugin", "github"])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: '--plugin' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("for the Git plugin", () => {
|
||||
it("correctly loads data", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGitData.mockResolvedValueOnce(undefined);
|
||||
expect(await run(load, ["foo/bar", "--plugin", "git"])).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGitData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGitData).toHaveBeenCalledWith({
|
||||
repoIds: [stringToRepoId("foo/bar")],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"bar",
|
||||
"git"
|
||||
it("the repo identifier is invalid", async () => {
|
||||
await expectFailure({
|
||||
args: ["missing_delimiter"],
|
||||
message: [
|
||||
expect.stringMatching(
|
||||
"^Error: Invalid repo string: missing_delimiter"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"bar",
|
||||
"git"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails if `loadGitData` rejects", async () => {
|
||||
loadGitData.mockRejectedValueOnce("please install Git");
|
||||
expect(await run(load, ["foo/bar", "--plugin", "git"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: ['"please install Git"'],
|
||||
});
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("succeeds for multiple repositories", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGitData.mockResolvedValueOnce(undefined);
|
||||
expect(
|
||||
await run(load, [
|
||||
"foo/bar",
|
||||
"foo/baz",
|
||||
"--output",
|
||||
"foo/combined",
|
||||
"--plugin",
|
||||
"git",
|
||||
])
|
||||
).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGitData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGitData).toHaveBeenCalledWith({
|
||||
repoIds: [stringToRepoId("foo/bar"), stringToRepoId("foo/baz")],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"combined",
|
||||
"git"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"combined",
|
||||
"git"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
describe("for the GitHub plugin", () => {
|
||||
it("correctly loads data", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGithubData.mockResolvedValueOnce(undefined);
|
||||
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGithubData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGithubData).toHaveBeenCalledWith({
|
||||
token: fakeGithubToken,
|
||||
repoIds: [stringToRepoId("foo/bar")],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"bar",
|
||||
"github"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"bar",
|
||||
"github"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails if a token is not provided", async () => {
|
||||
delete process.env.SOURCECRED_GITHUB_TOKEN;
|
||||
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
"fatal: no GitHub token specified",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("fails if `loadGithubData` rejects", async () => {
|
||||
loadGithubData.mockRejectedValueOnce("GitHub is down");
|
||||
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: ['"GitHub is down"'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when loading data for all plugins", () => {
|
||||
it("fails if a GitHub token is not provided", async () => {
|
||||
it("the SOURCECRED_GITHUB_TOKEN is unset", async () => {
|
||||
delete process.env.SOURCECRED_GITHUB_TOKEN;
|
||||
expect(await run(load, ["foo/bar"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [
|
||||
await expectFailure({
|
||||
args: ["missing_delimiter"],
|
||||
message: [
|
||||
"fatal: no GitHub token specified",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("invokes `execDependencyGraph` with a correct set of tasks", async () => {
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
expect(
|
||||
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"])
|
||||
).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
describe("the plugin flag", () => {
|
||||
it("not a valid plugin", async () => {
|
||||
await expectFailure({
|
||||
args: ["foo/bar", "--plugin", "foo"],
|
||||
message: [
|
||||
'fatal: unknown plugin: "foo"',
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
expect(execDependencyGraph).toHaveBeenCalledTimes(1);
|
||||
const tasks = execDependencyGraph.mock.calls[0][0];
|
||||
expect(tasks).toHaveLength(["git", "github"].length);
|
||||
expect(tasks.map((task) => task.id)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringMatching(/git(?!hub)/),
|
||||
expect.stringMatching(/github/),
|
||||
])
|
||||
);
|
||||
for (const task of tasks) {
|
||||
expect(task.cmd).toEqual([
|
||||
expect.stringMatching(/\bnode\b/),
|
||||
expect.stringMatching(/--max_old_space_size=/),
|
||||
process.argv[1],
|
||||
"load",
|
||||
"foo/bar",
|
||||
"foo/baz",
|
||||
"--output",
|
||||
"foo/combined",
|
||||
"--plugin",
|
||||
expect.stringMatching(/^(?:git|github)$/),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("properly infers the output when loading a single repository", async () => {
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
expect(await run(load, ["foo/bar"])).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
it("provided multiple times", async () => {
|
||||
await expectFailure({
|
||||
args: ["foo/bar", "--plugin", "git", "--plugin", "github"],
|
||||
message: [
|
||||
"fatal: '--plugin' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
expect(execDependencyGraph).toHaveBeenCalledTimes(1);
|
||||
const tasks = execDependencyGraph.mock.calls[0][0];
|
||||
for (const task of tasks) {
|
||||
expect(task.cmd).toEqual([
|
||||
expect.stringMatching(/\bnode\b/),
|
||||
expect.stringMatching(/--max_old_space_size=/),
|
||||
process.argv[1],
|
||||
"load",
|
||||
"foo/bar",
|
||||
"--output",
|
||||
"foo/bar",
|
||||
"--plugin",
|
||||
expect.stringMatching(/^(?:git|github)$/),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("fails if `execDependencyGraph` returns failure", async () => {
|
||||
execDependencyGraph.mockResolvedValueOnce({success: false});
|
||||
expect(
|
||||
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
it("provided multiple times with the same plugin", async () => {
|
||||
await expectFailure({
|
||||
args: ["foo/bar", "--plugin", "git", "--plugin", "git"],
|
||||
message: [
|
||||
"fatal: '--plugin' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("fails if `execDependencyGraph` rejects", async () => {
|
||||
execDependencyGraph.mockRejectedValueOnce({success: "definitely not"});
|
||||
expect(
|
||||
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: ['{"success":"definitely not"}'],
|
||||
it("provided without a value", async () => {
|
||||
await expectFailure({
|
||||
args: ["foo/bar", "--plugin"],
|
||||
message: [
|
||||
"fatal: '--plugin' given without value",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("writes a new repository registry if one does not exist", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]);
|
||||
const blob = fs
|
||||
.readFileSync(
|
||||
path.join(sourcecredDirectory, RepoIdRegistry.REPO_ID_REGISTRY_FILE)
|
||||
)
|
||||
.toString();
|
||||
const registry = RepoIdRegistry.fromJSON(JSON.parse(blob));
|
||||
const expected: RepoIdRegistry.RepoIdRegistry = [
|
||||
{repoId: stringToRepoId("foo/combined")},
|
||||
];
|
||||
expect(registry).toEqual(expected);
|
||||
});
|
||||
describe("the output flag is", () => {
|
||||
it("provided multiple times", async () => {
|
||||
await expectFailure({
|
||||
args: ["--output", "foo/bar", "--output", "bar/zod"],
|
||||
message: [
|
||||
"fatal: '--output' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("appends to an existing registry", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
fs.writeFileSync(
|
||||
path.join(sourcecredDirectory, RepoIdRegistry.REPO_ID_REGISTRY_FILE),
|
||||
JSON.stringify(
|
||||
RepoIdRegistry.toJSON([
|
||||
{repoId: stringToRepoId("previous/one")},
|
||||
{repoId: stringToRepoId("previous/two")},
|
||||
])
|
||||
)
|
||||
);
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]);
|
||||
const blob = fs
|
||||
.readFileSync(
|
||||
path.join(sourcecredDirectory, RepoIdRegistry.REPO_ID_REGISTRY_FILE)
|
||||
)
|
||||
.toString();
|
||||
const registry = RepoIdRegistry.fromJSON(JSON.parse(blob));
|
||||
const expected: RepoIdRegistry.RepoIdRegistry = [
|
||||
{repoId: stringToRepoId("previous/one")},
|
||||
{repoId: stringToRepoId("previous/two")},
|
||||
{repoId: stringToRepoId("foo/combined")},
|
||||
];
|
||||
expect(registry).toEqual(expected);
|
||||
it("provided multiple times with the same value", async () => {
|
||||
await expectFailure({
|
||||
args: ["--output", "foo/bar", "--output", "foo/bar"],
|
||||
message: [
|
||||
"fatal: '--output' given multiple times",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("not given a value", async () => {
|
||||
await expectFailure({
|
||||
args: ["--output"],
|
||||
message: [
|
||||
"fatal: '--output' given without value",
|
||||
"fatal: run 'sourcecred help load' for help",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("not a valid RepoId", async () => {
|
||||
await expectFailure({
|
||||
args: ["--output", "missing_delimiter"],
|
||||
message: [
|
||||
expect.stringMatching(
|
||||
"^Error: Invalid repo string: missing_delimiter"
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("processes options correctly", () => {
|
||||
function successCase({name, args, loadOptions}) {
|
||||
it(name + " (no plugin)", async () => {
|
||||
const {
|
||||
loadCommand,
|
||||
loadIndividualPlugin,
|
||||
loadDefaultPlugins,
|
||||
} = setup();
|
||||
expect(await run(loadCommand, args)).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
expect(loadIndividualPlugin).not.toHaveBeenCalled();
|
||||
expect(loadDefaultPlugins).toHaveBeenCalledWith(loadOptions);
|
||||
});
|
||||
it(name + " (with plugin)", async () => {
|
||||
const {
|
||||
loadCommand,
|
||||
loadIndividualPlugin,
|
||||
loadDefaultPlugins,
|
||||
} = setup();
|
||||
const pluginArgs = args.concat(["--plugin", "git"]);
|
||||
expect(await run(loadCommand, pluginArgs)).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
});
|
||||
expect(loadIndividualPlugin).toHaveBeenCalledWith(
|
||||
"git",
|
||||
loadOptions
|
||||
);
|
||||
expect(loadDefaultPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
const fooBar = makeRepoId("foo", "bar");
|
||||
const barZod = makeRepoId("bar", "zod");
|
||||
successCase({
|
||||
name: "with a single repository",
|
||||
args: ["foo/bar"],
|
||||
loadOptions: {output: fooBar, repoIds: [fooBar]},
|
||||
});
|
||||
successCase({
|
||||
name: "with a multiple repositories",
|
||||
args: ["foo/bar", "bar/zod", "--output", "bar/zod"],
|
||||
loadOptions: {output: barZod, repoIds: [fooBar, barZod]},
|
||||
});
|
||||
successCase({
|
||||
name: "with zero repositories",
|
||||
args: ["--output", "bar/zod"],
|
||||
loadOptions: {output: barZod, repoIds: []},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports to stderr if loadDefaultPlugins rejects", async () => {
|
||||
const {loadCommand, loadDefaultPlugins} = setup();
|
||||
loadDefaultPlugins.mockRejectedValueOnce(
|
||||
Error("loadDefaultPlugins failed.")
|
||||
);
|
||||
expect(await run(loadCommand, ["foo/bar"])).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: ["loadDefaultPlugins failed."],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports to stderr if loadIndividualPlugin rejects", async () => {
|
||||
const {loadCommand, loadIndividualPlugin} = setup();
|
||||
loadIndividualPlugin.mockRejectedValueOnce(
|
||||
Error("loadIndividualPlugin failed.")
|
||||
);
|
||||
expect(
|
||||
await run(loadCommand, ["foo/bar", "--plugin", "git"])
|
||||
).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: [],
|
||||
stderr: ["loadIndividualPlugin failed."],
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadIndividualPlugin", () => {
|
||||
const fooCombined = makeRepoId("foo", "combined");
|
||||
const fooBar = makeRepoId("foo", "bar");
|
||||
const fooBaz = makeRepoId("foo", "baz");
|
||||
|
||||
describe("for the Git plugin", () => {
|
||||
it("correctly loads data", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGitData.mockResolvedValueOnce(undefined);
|
||||
await loadIndividualPlugin("git", {
|
||||
repoIds: [fooBar],
|
||||
output: fooBar,
|
||||
});
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGitData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGitData).toHaveBeenCalledWith({
|
||||
repoIds: [fooBar],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"bar",
|
||||
"git"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"bar",
|
||||
"git"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects if `loadGitData` rejects", async () => {
|
||||
loadGitData.mockRejectedValueOnce(Error("please install Git"));
|
||||
const attempt = loadIndividualPlugin("git", {
|
||||
repoIds: [fooBar],
|
||||
output: fooBar,
|
||||
});
|
||||
expect(attempt).rejects.toThrow("please install Git");
|
||||
});
|
||||
});
|
||||
|
||||
it("succeeds for multiple repositories", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGitData.mockResolvedValueOnce(undefined);
|
||||
const options = {repoIds: [fooBar, fooBaz], output: fooCombined};
|
||||
await loadIndividualPlugin("git", options);
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGitData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGitData).toHaveBeenCalledWith({
|
||||
repoIds: [fooBar, fooBaz],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"combined",
|
||||
"git"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"combined",
|
||||
"git"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
describe("for the GitHub plugin", () => {
|
||||
it("correctly loads data", async () => {
|
||||
const sourcecredDirectory = newSourcecredDirectory();
|
||||
loadGithubData.mockResolvedValueOnce(undefined);
|
||||
const options = {repoIds: [fooBar], output: fooBar};
|
||||
await loadIndividualPlugin("github", options);
|
||||
|
||||
expect(execDependencyGraph).not.toHaveBeenCalled();
|
||||
expect(loadGithubData).toHaveBeenCalledTimes(1);
|
||||
expect(loadGithubData).toHaveBeenCalledWith({
|
||||
token: fakeGithubToken,
|
||||
repoIds: [fooBar],
|
||||
outputDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"data",
|
||||
"foo",
|
||||
"bar",
|
||||
"github"
|
||||
),
|
||||
cacheDirectory: path.join(
|
||||
sourcecredDirectory,
|
||||
"cache",
|
||||
"foo",
|
||||
"bar",
|
||||
"github"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails if a token is not provided", async () => {
|
||||
delete process.env.SOURCECRED_GITHUB_TOKEN;
|
||||
const result = loadIndividualPlugin("github", {
|
||||
repoIds: [fooBar],
|
||||
output: fooBar,
|
||||
});
|
||||
expect(result).rejects.toThrow("no SOURCECRED_GITHUB_TOKEN set");
|
||||
});
|
||||
|
||||
it("fails if `loadGithubData` rejects", async () => {
|
||||
loadGithubData.mockRejectedValueOnce(Error("GitHub is down"));
|
||||
const result = loadIndividualPlugin("github", {
|
||||
repoIds: [fooBar],
|
||||
output: fooBar,
|
||||
});
|
||||
expect(result).rejects.toThrow("GitHub is down");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadDefaultPlugins", () => {
|
||||
const fooCombined = makeRepoId("foo", "combined");
|
||||
const fooBar = makeRepoId("foo", "bar");
|
||||
const fooBaz = makeRepoId("foo", "baz");
|
||||
|
||||
it("creates a load sub-task per plugin", async () => {
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
await loadDefaultPlugins({
|
||||
output: fooCombined,
|
||||
repoIds: [fooBar, fooBaz],
|
||||
});
|
||||
expect(execDependencyGraph).toHaveBeenCalledTimes(1);
|
||||
const tasks = execDependencyGraph.mock.calls[0][0];
|
||||
expect(tasks).toHaveLength(["git", "github"].length);
|
||||
expect(tasks.map((task) => task.id)).toEqual(
|
||||
expect.arrayContaining(["load-git", "load-github"])
|
||||
);
|
||||
for (const task of tasks) {
|
||||
expect(task.cmd).toEqual([
|
||||
expect.stringMatching(/\bnode\b/),
|
||||
expect.stringMatching(/--max_old_space_size=/),
|
||||
process.argv[1],
|
||||
"load",
|
||||
"foo/bar",
|
||||
"foo/baz",
|
||||
"--output",
|
||||
"foo/combined",
|
||||
"--plugin",
|
||||
expect.stringMatching(/^(?:git|github)$/),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates RepoIdRegistry on success", async () => {
|
||||
const directory = newSourcecredDirectory();
|
||||
expect(RepoIdRegistry.getRegistry(directory)).toEqual(
|
||||
RepoIdRegistry.emptyRegistry()
|
||||
);
|
||||
execDependencyGraph.mockResolvedValueOnce({success: true});
|
||||
await loadDefaultPlugins({
|
||||
output: fooCombined,
|
||||
repoIds: [fooBar, fooBaz],
|
||||
});
|
||||
const expectedRegistry = RepoIdRegistry.addEntry(
|
||||
RepoIdRegistry.emptyRegistry(),
|
||||
{
|
||||
repoId: fooCombined,
|
||||
}
|
||||
);
|
||||
expect(RepoIdRegistry.getRegistry(directory)).toEqual(expectedRegistry);
|
||||
});
|
||||
|
||||
it("throws an error on execDependencyGraph failure", async () => {
|
||||
execDependencyGraph.mockResolvedValueOnce({success: false});
|
||||
const result = loadDefaultPlugins({
|
||||
output: fooCombined,
|
||||
repoIds: [fooBar, fooBaz],
|
||||
});
|
||||
|
||||
expect(result).rejects.toThrow("Load tasks failed.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue