From 17172c2d96745707aa0478e0afc685faa2970fad Mon Sep 17 00:00:00 2001 From: William Chargin Date: Sun, 2 Sep 2018 16:07:46 -0700 Subject: [PATCH] cli: implement `load` (#743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This ports the OClif version of `sourcecred load` to the sane CLI system. The functionality is similar, but the interface has been changed a bit (mostly simplifications): - The `SOURCECRED_GITHUB_TOKEN` can only be set by an environment variable, not by a command-line argument. This is standard practice because it is more secure: (a) other users on the same system can see the full command line arguments, but not the environment variables, and (b) it’s easier to accidentally leak a command line (e.g., in CI) than a full environment. - The `SOURCECRED_DIRECTORY` can only be set by an environment variable, not by a command-line argument. This is mostly just to simplify the interface, and also because we don’t really have a good name for the argument: we had previously used `-d`, which is unclear, but `--sourcecred-directory` is a bit redundant, while `--directory` is vague and `--sourcecred-directory` is redundant. This is an easy way out, but we can put the flag for this back in if it becomes a problem. - The `--max-old-space-size` argument has been removed in favor of a fixed value. It’s unlikely that users should need to change it. If we’re blowing an 8GB heap, we should try to not do that instead of increasing the heap. - Loading zero repositories, but specifying an output directory, is now valid. This is the right thing to do, but OClif got in our way in the previous implementation. Test Plan: Unit tests added, with full coverage; run `yarn unit`. To try it out, run `yarn backend`, then `node bin/cli.js load --help` to get started. I also manually tested that the following invocations work (i.e., they complete successfully, and `yarn start` shows good data): - `load sourcecred/sourcecred` - `load sourcecred/example-git{,hub} --output sourcecred/examples` These work even when invoked from a different directory. wchargin-branch: cli-load --- src/app/credExplorer/repoRegistry.js | 4 +- src/cli/help.js | 4 + src/cli/help.test.js | 10 + src/cli/load.js | 232 +++++++++++++ src/cli/load.test.js | 465 +++++++++++++++++++++++++++ src/cli/sourcecred.js | 3 + src/cli/sourcecred.test.js | 9 + 7 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 src/cli/load.js create mode 100644 src/cli/load.test.js diff --git a/src/app/credExplorer/repoRegistry.js b/src/app/credExplorer/repoRegistry.js index 651a912..76afc2b 100644 --- a/src/app/credExplorer/repoRegistry.js +++ b/src/app/credExplorer/repoRegistry.js @@ -1,8 +1,8 @@ // @flow // The repoRegistry is written by the CLI load command -// (src/oclif/commands/load.js) and is read by the RepositorySelect component -// (src/app/credExplorer/RepositorySelect.js) +// (src/oclif/commands/load.js; src/cli/load.js) and is read by the +// RepositorySelect component (src/app/credExplorer/RepositorySelect.js) import deepEqual from "lodash.isequal"; import {toCompat, fromCompat, type Compatible} from "../../util/compat"; import type {Repo} from "../../core/repo"; diff --git a/src/cli/help.js b/src/cli/help.js index 5b211b3..20ac8d9 100644 --- a/src/cli/help.js +++ b/src/cli/help.js @@ -4,6 +4,8 @@ import type {Command} from "./command"; import dedent from "../util/dedent"; +import {help as loadHelp} from "./load"; + const help: Command = async (args, std) => { if (args.length === 0) { usage(std.out); @@ -12,6 +14,7 @@ const help: Command = async (args, std) => { const command = args[0]; const subHelps: {[string]: Command} = { help: metaHelp, + load: loadHelp, }; if (subHelps[command] !== undefined) { return subHelps[command](args.slice(1), std); @@ -28,6 +31,7 @@ function usage(print: (string) => void): void { sourcecred [--version] [--help] Commands: + load load repository data into SourceCred help show this help message Use 'sourcecred help COMMAND' for help about an individual command. diff --git a/src/cli/help.test.js b/src/cli/help.test.js index 9d6f058..02281c6 100644 --- a/src/cli/help.test.js +++ b/src/cli/help.test.js @@ -25,6 +25,16 @@ describe("cli/help", () => { }); }); + it("prints help about 'sourcecred load'", async () => { + expect(await run(help, ["load"])).toEqual({ + exitCode: 0, + stdout: expect.arrayContaining([ + expect.stringMatching(/^usage: sourcecred load/), + ]), + stderr: [], + }); + }); + it("fails when given an unknown command", async () => { expect(await run(help, ["wat"])).toEqual({ exitCode: 1, diff --git a/src/cli/load.js b/src/cli/load.js new file mode 100644 index 0000000..6fcd25b --- /dev/null +++ b/src/cli/load.js @@ -0,0 +1,232 @@ +// @flow +// Implementation of `sourcecred load`. + +import fs from "fs"; +import stringify from "json-stable-stringify"; +import mkdirp from "mkdirp"; +import path from "path"; + +import * as RepoRegistry from "../app/credExplorer/repoRegistry"; +import {repoToString, stringToRepo, type Repo} from "../core/repo"; +import dedent from "../util/dedent"; +import type {Command} from "./command"; +import * as Common from "./common"; + +import {loadGithubData} from "../plugins/github/loadGithubData"; +import {loadGitData} from "../plugins/git/loadGitData"; + +const execDependencyGraph = require("../tools/execDependencyGraph").default; + +function usage(print: (string) => void): void { + print( + dedent`\ + usage: sourcecred load [REPO...] [--output REPO] + [--plugin PLUGIN] + [--help] + + Load a repository's data into SourceCred. + + Each REPO refers to a GitHub repository in the form OWNER/NAME: for + example, torvalds/linux. + + Arguments: + REPO... + Repositories for which to load data. + + --output REPO + Store the data under the name of this repository. When + loading multiple repositories, this can be the name of an + aggregate repository. For instance, if loading data for + repositories 'foo/bar' and 'foo/baz', the output name might + be 'foo/combined'. + + If only one repository is given, the output defaults to that + repository. Otherwise, an output must be specified. + + --plugin PLUGIN + Plugin for which to load data. Valid options are 'git' and + 'github'. If not specified, data for all plugins will be + loaded. + + --help + Show this help message and exit, as 'sourcecred help load'. + + Environment variables: + SOURCECRED_GITHUB_TOKEN + API token for GitHub. This should be a 40-character hex + string. Required if using the GitHub plugin; ignored + otherwise. + + To generate a token, create a "Personal access token" at + . When loading data for + public repositories, no special permissions are required. + For private repositories, the 'repo' scope is required. + + SOURCECRED_DIRECTORY + Directory owned by SourceCred, in which data, caches, + registries, etc. are stored. Optional: defaults to a + directory 'sourcecred' under your OS's temporary directory; + namely: + ${Common.defaultSourcecredDirectory()} + `.trimRight() + ); +} + +function die(std, message) { + std.err("fatal: " + message); + std.err("fatal: run 'sourcecred help load' for help"); + return 1; +} + +const load: Command = async (args, std) => { + const repos = []; + let explicitOutput: Repo | 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 = stringToRepo(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. + repos.push(stringToRepo(args[i])); + break; + } + } + } + + let output: Repo; + if (explicitOutput != null) { + output = explicitOutput; + } else if (repos.length === 1) { + output = repos[0]; + } else { + return die(std, "output repository not specified"); + } + + if (plugin == null) { + return loadDefaultPlugins({std, output, repos}); + } else { + return loadPlugin({std, output, repos, plugin}); + } +}; + +const loadDefaultPlugins = async ({std, output, repos}) => { + 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"); + } + + const tasks = [ + ...Common.defaultPlugins().map((pluginName) => ({ + id: `load-${pluginName}`, + cmd: [ + process.execPath, + "--max_old_space_size=8192", + process.argv[1], + "load", + ...repos.map((repo) => repoToString(repo)), + "--output", + repoToString(output), + "--plugin", + pluginName, + ], + deps: [], + })), + ]; + + const {success} = await execDependencyGraph(tasks, {taskPassLabel: "DONE"}); + if (success) { + addToRepoRegistry(output); + } + return success ? 0 : 1; +}; + +const loadPlugin = async ({std, output, repos, plugin}) => { + function scopedDirectory(key) { + const directory = path.join( + Common.sourcecredDirectory(), + key, + repoToString(output), + plugin + ); + mkdirp.sync(directory); + return directory; + } + const outputDirectory = scopedDirectory("data"); + 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"); + } + await loadGithubData({token, repos, outputDirectory, cacheDirectory}); + return 0; + } + case "git": + await loadGitData({repos, outputDirectory, cacheDirectory}); + return 0; + // 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))); + } +}; + +function addToRepoRegistry(repo) { + // TODO: Make this function transactional before loading repositories in + // parallel. + const outputFile = path.join( + Common.sourcecredDirectory(), + RepoRegistry.REPO_REGISTRY_FILE + ); + let registry = null; + if (fs.existsSync(outputFile)) { + const contents = fs.readFileSync(outputFile); + const registryJSON = JSON.parse(contents.toString()); + registry = RepoRegistry.fromJSON(registryJSON); + } else { + registry = RepoRegistry.emptyRegistry(); + } + registry = RepoRegistry.addRepo(repo, registry); + + fs.writeFileSync(outputFile, stringify(RepoRegistry.toJSON(registry))); +} + +export const help: Command = async (args, std) => { + if (args.length === 0) { + usage(std.out); + return 0; + } else { + usage(std.err); + return 1; + } +}; + +export default load; diff --git a/src/cli/load.test.js b/src/cli/load.test.js new file mode 100644 index 0000000..50cdb09 --- /dev/null +++ b/src/cli/load.test.js @@ -0,0 +1,465 @@ +// @flow + +import fs from "fs"; +import path from "path"; +import tmp from "tmp"; + +import {run} from "./testUtil"; +import load, {help} from "./load"; + +import * as RepoRegistry from "../app/credExplorer/repoRegistry"; +import {stringToRepo} from "../core/repo"; + +jest.mock("../tools/execDependencyGraph", () => ({ + default: jest.fn(), +})); +jest.mock("../plugins/github/loadGithubData", () => ({ + loadGithubData: jest.fn(), +})); +jest.mock("../plugins/git/loadGitData", () => ({ + loadGitData: jest.fn(), +})); + +type JestMockFn = $Call; +const execDependencyGraph: JestMockFn = (require("../tools/execDependencyGraph") + .default: any); +const loadGithubData: JestMockFn = (require("../plugins/github/loadGithubData") + .loadGithubData: any); +const loadGitData: JestMockFn = (require("../plugins/git/loadGitData") + .loadGitData: any); + +describe("cli/load", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Tests should call `newSourcecredDirectory` directly when they + // need the value. We call it here in case a test needs it to be set + // but does not care about the particular value. + newSourcecredDirectory(); + }); + + const fakeGithubToken = "....".replace(/./g, "0123456789"); + function newSourcecredDirectory() { + const dirname = tmp.dirSync().name; + process.env.SOURCECRED_DIRECTORY = dirname; + process.env.SOURCECRED_GITHUB_TOKEN = fakeGithubToken; + return dirname; + } + + describe("'help' command", () => { + it("prints usage when given no arguments", async () => { + expect(await run(help, [])).toEqual({ + exitCode: 0, + stdout: expect.arrayContaining([ + expect.stringMatching(/^usage: sourcecred load/), + ]), + stderr: [], + }); + }); + it("fails when given arguments", async () => { + expect(await run(help, ["foo/bar"])).toEqual({ + exitCode: 1, + stdout: [], + stderr: expect.arrayContaining([ + expect.stringMatching(/^usage: sourcecred load/), + ]), + }); + }); + }); + + describe("'load' command", () => { + it("prints usage with '--help'", async () => { + expect(await run(load, ["--help"])).toEqual({ + exitCode: 0, + stdout: expect.arrayContaining([ + expect.stringMatching(/^usage: sourcecred load/), + ]), + stderr: [], + }); + }); + + describe("for multiple repositories", () => { + it("fails when no output is specified for two repos", async () => { + expect( + await run(load, ["foo/bar", "foo/baz", "--plugin", "git"]) + ).toEqual({ + exitCode: 1, + stdout: [], + stderr: [ + "fatal: output repository not specified", + "fatal: run 'sourcecred help load' for help", + ], + }); + }); + it("fails when no output is specified for zero repos", 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("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({ + repos: [stringToRepo("foo/bar")], + outputDirectory: path.join( + sourcecredDirectory, + "data", + "foo", + "bar", + "git" + ), + 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({ + repos: [stringToRepo("foo/bar"), stringToRepo("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, + repos: [stringToRepo("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 () => { + delete process.env.SOURCECRED_GITHUB_TOKEN; + expect(await run(load, ["foo/bar"])).toEqual({ + 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 () => { + execDependencyGraph.mockResolvedValueOnce({success: true}); + expect( + await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]) + ).toEqual({ + exitCode: 0, + stdout: [], + stderr: [], + }); + 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: [], + }); + 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, RepoRegistry.REPO_REGISTRY_FILE) + ) + .toString(); + const registry = RepoRegistry.fromJSON(JSON.parse(blob)); + expect(registry).toEqual([stringToRepo("foo/combined")]); + }); + + it("appends to an existing registry", async () => { + const sourcecredDirectory = newSourcecredDirectory(); + fs.writeFileSync( + path.join(sourcecredDirectory, RepoRegistry.REPO_REGISTRY_FILE), + JSON.stringify( + RepoRegistry.toJSON([ + stringToRepo("previous/one"), + stringToRepo("previous/two"), + ]) + ) + ); + execDependencyGraph.mockResolvedValueOnce({success: true}); + await run(load, ["foo/bar", "foo/baz", "--output", "foo/combined"]); + const blob = fs + .readFileSync( + path.join(sourcecredDirectory, RepoRegistry.REPO_REGISTRY_FILE) + ) + .toString(); + const registry = RepoRegistry.fromJSON(JSON.parse(blob)); + expect(registry).toEqual([ + stringToRepo("previous/one"), + stringToRepo("previous/two"), + stringToRepo("foo/combined"), + ]); + }); + }); + }); +}); diff --git a/src/cli/sourcecred.js b/src/cli/sourcecred.js index ea33c97..67afbd5 100644 --- a/src/cli/sourcecred.js +++ b/src/cli/sourcecred.js @@ -6,6 +6,7 @@ import type {Command} from "./command"; import {VERSION_SHORT} from "../app/version"; import help from "./help"; +import load from "./load"; const sourcecred: Command = async (args, std) => { if (args.length === 0) { @@ -19,6 +20,8 @@ const sourcecred: Command = async (args, std) => { case "--help": case "help": return help(args.slice(1), std); + case "load": + return load(args.slice(1), std); default: std.err("fatal: unknown command: " + JSON.stringify(args[0])); std.err("fatal: run 'sourcecred help' for commands and usage"); diff --git a/src/cli/sourcecred.test.js b/src/cli/sourcecred.test.js index ca129aa..1a37461 100644 --- a/src/cli/sourcecred.test.js +++ b/src/cli/sourcecred.test.js @@ -12,6 +12,7 @@ function mockCommand(name) { } jest.mock("./help", () => mockCommand("help")); +jest.mock("./load", () => mockCommand("load")); describe("cli/sourcecred", () => { it("fails with usage when invoked with no arguments", async () => { @@ -46,6 +47,14 @@ describe("cli/sourcecred", () => { }); }); + it("responds to 'load'", async () => { + expect(await run(sourcecred, ["load", "foo/bar", "foo/baz"])).toEqual({ + exitCode: 2, + stdout: ['out(load): ["foo/bar","foo/baz"]'], + stderr: ["err(load)"], + }); + }); + it("fails given an unknown command", async () => { expect(await run(sourcecred, ["wat"])).toEqual({ exitCode: 1,