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 {help as loadHelp} from "./load";
import {help as analyzeHelp} from "./analyze";
const help: Command = async (args, std) => {
if (args.length === 0) {
@ -15,6 +16,7 @@ const help: Command = async (args, std) => {
const subHelps: {[string]: Command} = {
help: metaHelp,
load: loadHelp,
analyze: analyzeHelp,
};
if (subHelps[command] !== undefined) {
return subHelps[command](args.slice(1), std);
@ -32,6 +34,7 @@ function usage(print: (string) => void): void {
Commands:
load load repository data into SourceCred
analyze analyze cred for a loaded repository
help show this help message
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 () => {
expect(await run(help, ["wat"])).toEqual({
exitCode: 1,

View File

@ -7,6 +7,7 @@ import {VERSION_SHORT} from "../app/version";
import help from "./help";
import load from "./load";
import analyze from "./analyze";
const sourcecred: Command = async (args, std) => {
if (args.length === 0) {
@ -22,6 +23,8 @@ const sourcecred: Command = async (args, std) => {
return help(args.slice(1), std);
case "load":
return load(args.slice(1), std);
case "analyze":
return analyze(args.slice(1), std);
default:
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
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("./load", () => mockCommand("load"));
jest.mock("./analyze", () => mockCommand("analyze"));
describe("cli/sourcecred", () => {
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 () => {
expect(await run(sourcecred, ["wat"])).toEqual({
exitCode: 1,