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:
Dandelion Mané 2019-04-11 18:59:08 +02:00 committed by GitHub
parent 13a90675a8
commit 320a69759e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 516 additions and 427 deletions

View File

@ -4,6 +4,8 @@
import mkdirp from "mkdirp"; import mkdirp from "mkdirp";
import path from "path"; import path from "path";
import * as NullUtil from "../util/null";
import * as RepoIdRegistry from "../core/repoIdRegistry"; import * as RepoIdRegistry from "../core/repoIdRegistry";
import {repoIdToString, stringToRepoId, type RepoId} from "../core/repoId"; import {repoIdToString, stringToRepoId, type RepoId} from "../core/repoId";
import dedent from "../util/dedent"; import dedent from "../util/dedent";
@ -75,10 +77,29 @@ function die(std, message) {
return 1; return 1;
} }
const load: Command = async (args, std) => { export type LoadOptions = {|
const repoIds = []; +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 explicitOutput: RepoId | null = null;
let plugin: Common.PluginName | null = null; let plugin: Common.PluginName | null = null;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
switch (args[i]) { switch (args[i]) {
case "--help": { case "--help": {
@ -94,7 +115,8 @@ const load: Command = async (args, std) => {
break; break;
} }
case "--plugin": { case "--plugin": {
if (plugin != null) return die(std, "'--plugin' given multiple times"); if (plugin != null)
return die(std, "'--plugin' given multiple times");
if (++i >= args.length) if (++i >= args.length)
return die(std, "'--plugin' given without value"); return die(std, "'--plugin' given without value");
const arg = args[i]; const arg = args[i];
@ -120,21 +142,29 @@ const load: Command = async (args, std) => {
return die(std, "output repository not specified"); return die(std, "output repository not specified");
} }
const options: LoadOptions = {output, repoIds: repoIds};
if (plugin == null) { if (plugin == null) {
return loadDefaultPlugins({std, output, repoIds}); try {
await loadDefaultPlugins(options);
return 0;
} catch (e) {
std.err(e.message);
return 1;
}
} else { } else {
return loadPlugin({std, output, repoIds, plugin}); try {
await loadIndividualPlugin(plugin, options);
return 0;
} catch (e) {
std.err(e.message);
return 1;
} }
};
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");
} }
};
}
export const loadDefaultPlugins = async (options: LoadOptions) => {
const tasks = [ const tasks = [
...Common.defaultPlugins().map((pluginName) => ({ ...Common.defaultPlugins().map((pluginName) => ({
id: `load-${pluginName}`, id: `load-${pluginName}`,
@ -143,9 +173,9 @@ const loadDefaultPlugins = async ({std, output, repoIds}) => {
"--max_old_space_size=8192", "--max_old_space_size=8192",
process.argv[1], process.argv[1],
"load", "load",
...repoIds.map((repoId) => repoIdToString(repoId)), ...options.repoIds.map((repoId) => repoIdToString(repoId)),
"--output", "--output",
repoIdToString(output), repoIdToString(options.output),
"--plugin", "--plugin",
pluginName, pluginName,
], ],
@ -155,12 +185,18 @@ const loadDefaultPlugins = async ({std, output, repoIds}) => {
const {success} = await execDependencyGraph(tasks, {taskPassLabel: "DONE"}); const {success} = await execDependencyGraph(tasks, {taskPassLabel: "DONE"});
if (success) { 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) { function scopedDirectory(key) {
const directory = path.join( const directory = path.join(
Common.sourcecredDirectory(), Common.sourcecredDirectory(),
@ -175,24 +211,18 @@ const loadPlugin = async ({std, output, repoIds, plugin}) => {
const cacheDirectory = scopedDirectory("cache"); const cacheDirectory = scopedDirectory("cache");
switch (plugin) { switch (plugin) {
case "github": { case "github": {
const token = Common.githubToken(); const token = NullUtil.get(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");
}
await loadGithubData({token, repoIds, outputDirectory, cacheDirectory}); await loadGithubData({token, repoIds, outputDirectory, cacheDirectory});
return 0; return;
} }
case "git": case "git":
await loadGitData({repoIds, outputDirectory, cacheDirectory}); await loadGitData({repoIds, outputDirectory, cacheDirectory});
return 0; return;
// Unlike the previous check, which was validating user input and // Unlike the previous check, which was validating user input and
// was reachable, this really should not occur. // was reachable, this really should not occur.
// istanbul ignore next // istanbul ignore next
default: 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; export default load;

View File

@ -1,14 +1,18 @@
// @flow // @flow
import fs from "fs";
import path from "path"; import path from "path";
import tmp from "tmp"; import tmp from "tmp";
import {run} from "./testUtil"; import {run} from "./testUtil";
import load, {help} from "./load"; import {
makeLoadCommand,
loadDefaultPlugins,
loadIndividualPlugin,
help,
} from "./load";
import * as RepoIdRegistry from "../core/repoIdRegistry"; import * as RepoIdRegistry from "../core/repoIdRegistry";
import {stringToRepoId} from "../core/repoId"; import {makeRepoId} from "../core/repoId";
jest.mock("../tools/execDependencyGraph", () => jest.fn()); jest.mock("../tools/execDependencyGraph", () => jest.fn());
jest.mock("../plugins/github/loadGithubData", () => ({ 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 () => { it("prints usage with '--help'", async () => {
expect(await run(load, ["--help"])).toEqual({ const {loadCommand} = setup();
expect(await run(loadCommand, ["--help"])).toEqual({
exitCode: 0, exitCode: 0,
stdout: expect.arrayContaining([ stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred load/), expect.stringMatching(/^usage: sourcecred load/),
@ -74,137 +89,269 @@ describe("cli/load", () => {
}); });
}); });
describe("for multiple repositories", () => { it("calls loadDefaultPlugins if plugin not specified", async () => {
it("fails when no output is specified for two repoIds", async () => { const {loadCommand, loadDefaultPlugins} = setup();
expect( const invocation = run(loadCommand, ["foo/bar"]);
await run(load, ["foo/bar", "foo/baz", "--plugin", "git"]) expect(await invocation).toEqual({
).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, exitCode: 1,
stdout: [], 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: output repository not specified",
"fatal: run 'sourcecred help load' for help", "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: [
"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("multiple repos provided without output repository", async () => {
it("fails for an unknown plugin", async () => { await expectFailure({
expect(await run(load, ["foo/bar", "--plugin", "wat"])).toEqual({ args: ["foo/bar", "zoink/zod"],
exitCode: 1, message: [
stdout: [], "fatal: output repository not specified",
stderr: [
'fatal: unknown plugin: "wat"',
"fatal: run 'sourcecred help load' for help", "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({ it("the repo identifier is invalid", async () => {
exitCode: 1, await expectFailure({
stdout: [], args: ["missing_delimiter"],
stderr: [ message: [
expect.stringMatching(
"^Error: Invalid repo string: missing_delimiter"
),
],
});
});
it("the SOURCECRED_GITHUB_TOKEN is unset", async () => {
delete process.env.SOURCECRED_GITHUB_TOKEN;
await expectFailure({
args: ["missing_delimiter"],
message: [
"fatal: no GitHub token specified",
"fatal: run 'sourcecred help load' for help",
],
});
});
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",
],
});
});
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",
],
});
});
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("provided without a value", async () => {
await expectFailure({
args: ["foo/bar", "--plugin"],
message: [
"fatal: '--plugin' given without value", "fatal: '--plugin' given without value",
"fatal: run 'sourcecred help load' for help", "fatal: run 'sourcecred help load' for help",
], ],
}); });
}); });
it("fails when the same plugin is specified multiple times", async () => {
expect( describe("the output flag is", () => {
await run(load, ["foo/bar", "--plugin", "git", "--plugin", "git"]) it("provided multiple times", async () => {
).toEqual({ await expectFailure({
exitCode: 1, args: ["--output", "foo/bar", "--output", "bar/zod"],
stdout: [], message: [
stderr: [ "fatal: '--output' given multiple times",
"fatal: '--plugin' given multiple times",
"fatal: run 'sourcecred help load' for help", "fatal: run 'sourcecred help load' for help",
], ],
}); });
}); });
it("fails when multiple plugins are specified", async () => {
expect( it("provided multiple times with the same value", async () => {
await run(load, ["foo/bar", "--plugin", "git", "--plugin", "github"]) await expectFailure({
).toEqual({ args: ["--output", "foo/bar", "--output", "foo/bar"],
exitCode: 1, message: [
stdout: [], "fatal: '--output' given multiple times",
stderr: [
"fatal: '--plugin' given multiple times",
"fatal: run 'sourcecred help load' for help", "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", () => { describe("for the Git plugin", () => {
it("correctly loads data", async () => { it("correctly loads data", async () => {
const sourcecredDirectory = newSourcecredDirectory(); const sourcecredDirectory = newSourcecredDirectory();
loadGitData.mockResolvedValueOnce(undefined); loadGitData.mockResolvedValueOnce(undefined);
expect(await run(load, ["foo/bar", "--plugin", "git"])).toEqual({ await loadIndividualPlugin("git", {
exitCode: 0, repoIds: [fooBar],
stdout: [], output: fooBar,
stderr: [],
}); });
expect(execDependencyGraph).not.toHaveBeenCalled(); expect(execDependencyGraph).not.toHaveBeenCalled();
expect(loadGitData).toHaveBeenCalledTimes(1); expect(loadGitData).toHaveBeenCalledTimes(1);
expect(loadGitData).toHaveBeenCalledWith({ expect(loadGitData).toHaveBeenCalledWith({
repoIds: [stringToRepoId("foo/bar")], repoIds: [fooBar],
outputDirectory: path.join( outputDirectory: path.join(
sourcecredDirectory, sourcecredDirectory,
"data", "data",
@ -222,38 +369,26 @@ describe("cli/load", () => {
}); });
}); });
it("fails if `loadGitData` rejects", async () => { it("rejects if `loadGitData` rejects", async () => {
loadGitData.mockRejectedValueOnce("please install Git"); loadGitData.mockRejectedValueOnce(Error("please install Git"));
expect(await run(load, ["foo/bar", "--plugin", "git"])).toEqual({ const attempt = loadIndividualPlugin("git", {
exitCode: 1, repoIds: [fooBar],
stdout: [], output: fooBar,
stderr: ['"please install Git"'],
}); });
expect(attempt).rejects.toThrow("please install Git");
}); });
}); });
it("succeeds for multiple repositories", async () => { it("succeeds for multiple repositories", async () => {
const sourcecredDirectory = newSourcecredDirectory(); const sourcecredDirectory = newSourcecredDirectory();
loadGitData.mockResolvedValueOnce(undefined); loadGitData.mockResolvedValueOnce(undefined);
expect( const options = {repoIds: [fooBar, fooBaz], output: fooCombined};
await run(load, [ await loadIndividualPlugin("git", options);
"foo/bar",
"foo/baz",
"--output",
"foo/combined",
"--plugin",
"git",
])
).toEqual({
exitCode: 0,
stdout: [],
stderr: [],
});
expect(execDependencyGraph).not.toHaveBeenCalled(); expect(execDependencyGraph).not.toHaveBeenCalled();
expect(loadGitData).toHaveBeenCalledTimes(1); expect(loadGitData).toHaveBeenCalledTimes(1);
expect(loadGitData).toHaveBeenCalledWith({ expect(loadGitData).toHaveBeenCalledWith({
repoIds: [stringToRepoId("foo/bar"), stringToRepoId("foo/baz")], repoIds: [fooBar, fooBaz],
outputDirectory: path.join( outputDirectory: path.join(
sourcecredDirectory, sourcecredDirectory,
"data", "data",
@ -275,17 +410,14 @@ describe("cli/load", () => {
it("correctly loads data", async () => { it("correctly loads data", async () => {
const sourcecredDirectory = newSourcecredDirectory(); const sourcecredDirectory = newSourcecredDirectory();
loadGithubData.mockResolvedValueOnce(undefined); loadGithubData.mockResolvedValueOnce(undefined);
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({ const options = {repoIds: [fooBar], output: fooBar};
exitCode: 0, await loadIndividualPlugin("github", options);
stdout: [],
stderr: [],
});
expect(execDependencyGraph).not.toHaveBeenCalled(); expect(execDependencyGraph).not.toHaveBeenCalled();
expect(loadGithubData).toHaveBeenCalledTimes(1); expect(loadGithubData).toHaveBeenCalledTimes(1);
expect(loadGithubData).toHaveBeenCalledWith({ expect(loadGithubData).toHaveBeenCalledWith({
token: fakeGithubToken, token: fakeGithubToken,
repoIds: [stringToRepoId("foo/bar")], repoIds: [fooBar],
outputDirectory: path.join( outputDirectory: path.join(
sourcecredDirectory, sourcecredDirectory,
"data", "data",
@ -305,57 +437,43 @@ describe("cli/load", () => {
it("fails if a token is not provided", async () => { it("fails if a token is not provided", async () => {
delete process.env.SOURCECRED_GITHUB_TOKEN; delete process.env.SOURCECRED_GITHUB_TOKEN;
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({ const result = loadIndividualPlugin("github", {
exitCode: 1, repoIds: [fooBar],
stdout: [], output: fooBar,
stderr: [
"fatal: no GitHub token specified",
"fatal: run 'sourcecred help load' for help",
],
}); });
expect(result).rejects.toThrow("no SOURCECRED_GITHUB_TOKEN set");
}); });
it("fails if `loadGithubData` rejects", async () => { it("fails if `loadGithubData` rejects", async () => {
loadGithubData.mockRejectedValueOnce("GitHub is down"); loadGithubData.mockRejectedValueOnce(Error("GitHub is down"));
expect(await run(load, ["foo/bar", "--plugin", "github"])).toEqual({ const result = loadIndividualPlugin("github", {
exitCode: 1, repoIds: [fooBar],
stdout: [], output: fooBar,
stderr: ['"GitHub is down"'], });
expect(result).rejects.toThrow("GitHub is down");
});
});
}); });
}); });
}); });
}); });
describe("when loading data for all plugins", () => { describe("loadDefaultPlugins", () => {
it("fails if a GitHub token is not provided", async () => { const fooCombined = makeRepoId("foo", "combined");
delete process.env.SOURCECRED_GITHUB_TOKEN; const fooBar = makeRepoId("foo", "bar");
expect(await run(load, ["foo/bar"])).toEqual({ const fooBaz = makeRepoId("foo", "baz");
exitCode: 1,
stdout: [],
stderr: [
"fatal: no GitHub token specified",
"fatal: run 'sourcecred help load' for help",
],
});
});
it("invokes `execDependencyGraph` with a correct set of tasks", async () => { it("creates a load sub-task per plugin", async () => {
execDependencyGraph.mockResolvedValueOnce({success: true}); execDependencyGraph.mockResolvedValueOnce({success: true});
expect( await loadDefaultPlugins({
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]) output: fooCombined,
).toEqual({ repoIds: [fooBar, fooBaz],
exitCode: 0,
stdout: [],
stderr: [],
}); });
expect(execDependencyGraph).toHaveBeenCalledTimes(1); expect(execDependencyGraph).toHaveBeenCalledTimes(1);
const tasks = execDependencyGraph.mock.calls[0][0]; const tasks = execDependencyGraph.mock.calls[0][0];
expect(tasks).toHaveLength(["git", "github"].length); expect(tasks).toHaveLength(["git", "github"].length);
expect(tasks.map((task) => task.id)).toEqual( expect(tasks.map((task) => task.id)).toEqual(
expect.arrayContaining([ expect.arrayContaining(["load-git", "load-github"])
expect.stringMatching(/git(?!hub)/),
expect.stringMatching(/github/),
])
); );
for (const task of tasks) { for (const task of tasks) {
expect(task.cmd).toEqual([ expect(task.cmd).toEqual([
@ -373,94 +491,33 @@ describe("cli/load", () => {
} }
}); });
it("properly infers the output when loading a single repository", async () => { it("updates RepoIdRegistry on success", async () => {
execDependencyGraph.mockResolvedValueOnce({success: true}); const directory = newSourcecredDirectory();
expect(await run(load, ["foo/bar"])).toEqual({ expect(RepoIdRegistry.getRegistry(directory)).toEqual(
exitCode: 0, RepoIdRegistry.emptyRegistry()
stdout: [],
stderr: [],
});
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("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("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);
});
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}); execDependencyGraph.mockResolvedValueOnce({success: true});
await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]); await loadDefaultPlugins({
const blob = fs output: fooCombined,
.readFileSync( repoIds: [fooBar, fooBaz],
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);
}); });
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.");
}); });
}); });
}); });