Add skeleton of sourcecred analyze (#942)

The `analyze` command is the first step towards #704 and #703. When
fully implemented, it will run PageRank for a loaded repository,
generating a complete graph and cred attribution.

For now, this just adds a scaffold. It does basic argument parsing, and
has help text, but the actual command is not yet implemented.

Test plan:
Unit tests verify that the analyze command is hooked into `sourcecred`
and `sourcecred help`, and that it responds to the `--help` command and
parses its arguments appropriately.
This commit is contained in:
Dandelion Mané 2018-10-29 22:27:06 +00:00 committed by GitHub
parent d3e79e3c4e
commit 542e2f9723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 0 deletions

84
src/cli/analyze.js Normal file
View File

@ -0,0 +1,84 @@
// @flow
// Implementation of `sourcecred analyze`
import {stringToRepoId, repoIdToString, type RepoId} from "../core/repoId";
import dedent from "../util/dedent";
import type {Command} from "./command";
import * as Common from "./common";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred analyze REPO_ID
sourcecred analyze --help
Analyze a loaded repository, generating a cred attribution for it.
REPO_ID refers to a GitHub repository in the form OWNER/NAME: for
example, torvalds/linux. The REPO_ID may be an 'aggregated
repository' generated via the \`--output\` flag to \`sourcecred
load\`
Note: This command is not yet implemented.
Arguments:
REPO_ID
Repository to analyze
--help
Show this help message and exit, as 'sourcecred help analyze'.
Environment variables:
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 analyze' for help");
return 1;
}
const analyze: Command = async (args, std) => {
let repoId: RepoId | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
default: {
// Should be a repository
if (repoId != null) {
return die(std, "multiple repositories provided");
}
repoId = stringToRepoId(args[i]);
break;
}
}
}
if (repoId == null) {
return die(std, "repository not specified");
}
std.out(`would analyze ${repoIdToString(repoId)}, but not yet implemented`);
return 0;
};
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};
export default analyze;

81
src/cli/analyze.test.js Normal file
View File

@ -0,0 +1,81 @@
// @flow
import {run} from "./testUtil";
import analyze, {help} from "./analyze";
describe("cli/analyze", () => {
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 analyze/),
]),
stderr: [],
});
});
it("fails when given arguments", async () => {
expect(await run(help, ["foo/bar"])).toEqual({
exitCode: 1,
stdout: [],
stderr: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred analyze/),
]),
});
});
});
describe("'analyze' command", () => {
it("prints usage with '--help'", async () => {
expect(await run(analyze, ["--help"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred analyze/),
]),
stderr: [],
});
});
it("errors if no repository is specified", async () => {
expect(await run(analyze, [])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: repository not specified",
"fatal: run 'sourcecred help analyze' for help",
],
});
});
it("errors if multiple repositories are specified", async () => {
expect(await run(analyze, ["foo/bar", "zoink/zod"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: multiple repositories provided",
"fatal: run 'sourcecred help analyze' for help",
],
});
});
it("errors if provided a invalid repository", async () => {
expect(await run(analyze, ["--zoomzoom"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
expect.stringContaining("Error: Invalid repo string: --zoomzoom"),
],
});
});
it("prints a not-yet-implemented message for a valid repo", async () => {
expect(await run(analyze, ["sourcecred/example-github"])).toEqual({
exitCode: 0,
stdout: [
"would analyze sourcecred/example-github, but not yet implemented",
],
stderr: [],
});
});
});
});

View File

@ -5,6 +5,7 @@ import type {Command} from "./command";
import dedent from "../util/dedent"; import dedent from "../util/dedent";
import {help as loadHelp} from "./load"; import {help as loadHelp} from "./load";
import {help as analyzeHelp} from "./analyze";
const help: Command = async (args, std) => { const help: Command = async (args, std) => {
if (args.length === 0) { if (args.length === 0) {
@ -15,6 +16,7 @@ const help: Command = async (args, std) => {
const subHelps: {[string]: Command} = { const subHelps: {[string]: Command} = {
help: metaHelp, help: metaHelp,
load: loadHelp, load: loadHelp,
analyze: analyzeHelp,
}; };
if (subHelps[command] !== undefined) { if (subHelps[command] !== undefined) {
return subHelps[command](args.slice(1), std); return subHelps[command](args.slice(1), std);
@ -32,6 +34,7 @@ function usage(print: (string) => void): void {
Commands: Commands:
load load repository data into SourceCred load load repository data into SourceCred
analyze analyze cred for a loaded repository
help show this help message help show this help message
Use 'sourcecred help COMMAND' for help about an individual command. Use 'sourcecred help COMMAND' for help about an individual command.

View File

@ -35,6 +35,16 @@ describe("cli/help", () => {
}); });
}); });
it("prints help about 'sourcecred analyze'", async () => {
expect(await run(help, ["analyze"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred analyze/),
]),
stderr: [],
});
});
it("fails when given an unknown command", async () => { it("fails when given an unknown command", async () => {
expect(await run(help, ["wat"])).toEqual({ expect(await run(help, ["wat"])).toEqual({
exitCode: 1, exitCode: 1,

View File

@ -7,6 +7,7 @@ import {VERSION_SHORT} from "../app/version";
import help from "./help"; import help from "./help";
import load from "./load"; import load from "./load";
import analyze from "./analyze";
const sourcecred: Command = async (args, std) => { const sourcecred: Command = async (args, std) => {
if (args.length === 0) { if (args.length === 0) {
@ -22,6 +23,8 @@ const sourcecred: Command = async (args, std) => {
return help(args.slice(1), std); return help(args.slice(1), std);
case "load": case "load":
return load(args.slice(1), std); return load(args.slice(1), std);
case "analyze":
return analyze(args.slice(1), std);
default: default:
std.err("fatal: unknown command: " + JSON.stringify(args[0])); std.err("fatal: unknown command: " + JSON.stringify(args[0]));
std.err("fatal: run 'sourcecred help' for commands and usage"); std.err("fatal: run 'sourcecred help' for commands and usage");

View File

@ -13,6 +13,7 @@ function mockCommand(name) {
jest.mock("./help", () => mockCommand("help")); jest.mock("./help", () => mockCommand("help"));
jest.mock("./load", () => mockCommand("load")); jest.mock("./load", () => mockCommand("load"));
jest.mock("./analyze", () => mockCommand("analyze"));
describe("cli/sourcecred", () => { describe("cli/sourcecred", () => {
it("fails with usage when invoked with no arguments", async () => { it("fails with usage when invoked with no arguments", async () => {
@ -55,6 +56,14 @@ describe("cli/sourcecred", () => {
}); });
}); });
it("responds to 'analyze'", async () => {
expect(await run(sourcecred, ["analyze", "foo/bar", "foo/baz"])).toEqual({
exitCode: 2,
stdout: ['out(analyze): ["foo/bar","foo/baz"]'],
stderr: ["err(analyze)"],
});
});
it("fails given an unknown command", async () => { it("fails given an unknown command", async () => {
expect(await run(sourcecred, ["wat"])).toEqual({ expect(await run(sourcecred, ["wat"])).toEqual({
exitCode: 1, exitCode: 1,