Share default TimelineCredParameters (#1376)

At present, every place in the codebase that needs
TimelineCredParameters constructs them ad-hoc, meaning we don't have any
shared defaults across different consumers.

This commit adds a new type, `PartialTimelineCredParameters`, which
is basically `TimelineCredParameters` with every field marked optional.
Callers can then choose to override any fields where they want
non-default values. A new internal `partialParams` function promotes
these partial parameters to full parameters.

All the public interfaces for using params (namely,
`TimelineCred.compute` and `TimelineCred.reanalyze`) now accept optional
partial params. If the params are not specified, default values are
used; if partial params are provided, all the explicitly provided values
are used, and unspecified values are initialized to default values.

Test plan: A simple unit test was added to ensure that weights overrides
work as intended. `git grep "intervalDecay: "` reveals that there are no
other explicit parameter constructions in the codebase. All existing
unit tests pass.
This commit is contained in:
Dandelion Mané 2019-09-12 15:21:13 +02:00 committed by GitHub
parent def1fef192
commit 0a0010f38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 42 deletions

View File

@ -5,6 +5,7 @@ import {
type WeightsJSON, type WeightsJSON,
toJSON as weightsToJSON, toJSON as weightsToJSON,
fromJSON as weightsFromJSON, fromJSON as weightsFromJSON,
defaultWeights,
} from "../weights"; } from "../weights";
/** /**
@ -31,6 +32,20 @@ export type TimelineCredParameters = {|
+weights: Weights, +weights: Weights,
|}; |};
export const DEFAULT_ALPHA = 0.05;
export const DEFAULT_INTERVAL_DECAY = 0.5;
/**
* The PartialTimelineCredParameters are a version of TimelineCredParameters
* where every field has been marked optional, to make it convenient for API
* clients to override just the parameters they want to.
*/
export type PartialTimelineCredParameters = {|
+alpha?: number,
+intervalDecay?: number,
+weights?: Weights,
|};
export type TimelineCredParametersJSON = {| export type TimelineCredParametersJSON = {|
+alpha: number, +alpha: number,
+intervalDecay: number, +intervalDecay: number,
@ -56,3 +71,32 @@ export function paramsFromJSON(
weights: weightsFromJSON(p.weights), weights: weightsFromJSON(p.weights),
}; };
} }
/**
* Exports the default TimelineCredParameters.
*
* End consumers of SourceCred will not need to depend on this; it's
* provided for implementation of SourceCred's APIs.
*/
export function defaultParams(): TimelineCredParameters {
return {
alpha: DEFAULT_ALPHA,
intervalDecay: DEFAULT_INTERVAL_DECAY,
weights: defaultWeights(),
};
}
/**
* Promote PartialTimelineCredParameters to TimelineCredParameters.
*
* This takes PartialTimelineCredParameters and mixes them with the
* default parameters to provide a full TimelineCredParameters.
*
* End consumers of SourceCred will not need to depend on this; it's
* provided for implementation of SourceCred's APIs.
*/
export function partialParams(
partial: PartialTimelineCredParameters
): TimelineCredParameters {
return {...defaultParams(), ...partial};
}

View File

@ -1,19 +1,67 @@
// @flow // @flow
import {paramsToJSON, paramsFromJSON} from "./params"; import {
paramsToJSON,
paramsFromJSON,
defaultParams,
partialParams,
type TimelineCredParameters,
DEFAULT_ALPHA,
DEFAULT_INTERVAL_DECAY,
} from "./params";
import {defaultWeights} from "../weights"; import {defaultWeights} from "../weights";
import {NodeAddress} from "../../core/graph"; import {NodeAddress} from "../../core/graph";
describe("analysis/timeline/params", () => { describe("analysis/timeline/params", () => {
it("JSON round trip", () => { const customWeights = () => {
const weights = defaultWeights(); const weights = defaultWeights();
// Ensure it works with non-default weights // Ensure it works with non-default weights
weights.nodeManualWeights.set(NodeAddress.empty, 33); weights.nodeManualWeights.set(NodeAddress.empty, 33);
const p = {alpha: 0.5, intervalDecay: 0.5, weights}; return weights;
};
it("JSON round trip", () => {
const p: TimelineCredParameters = {
alpha: 0.1337,
intervalDecay: 0.31337,
weights: customWeights(),
};
const j = paramsToJSON(p); const j = paramsToJSON(p);
const p_ = paramsFromJSON(j); const p_ = paramsFromJSON(j);
const j_ = paramsToJSON(p_); const j_ = paramsToJSON(p_);
expect(j).toEqual(j_); expect(j).toEqual(j_);
expect(p).toEqual(p_); expect(p).toEqual(p_);
}); });
it("defaultParams", () => {
const expected: TimelineCredParameters = {
alpha: DEFAULT_ALPHA,
intervalDecay: DEFAULT_INTERVAL_DECAY,
weights: defaultWeights(),
};
expect(defaultParams()).toEqual(expected);
});
describe("partialParams", () => {
it("uses default values if no overrides provided", () => {
const params = partialParams({});
expect(params).toEqual(defaultParams());
});
it("accepts an alpha override", () => {
const params = partialParams({alpha: 0.99});
expect(params.weights).toEqual(defaultWeights());
expect(params.alpha).toEqual(0.99);
expect(params.intervalDecay).toEqual(DEFAULT_INTERVAL_DECAY);
});
it("accepts weights override", () => {
const weights = customWeights();
const params = partialParams({weights});
expect(params.weights).toEqual(weights);
expect(params.alpha).toEqual(DEFAULT_ALPHA);
expect(params.intervalDecay).toEqual(DEFAULT_INTERVAL_DECAY);
});
it("accepts intervalDecay override", () => {
const params = partialParams({intervalDecay: 0.1});
expect(params.weights).toEqual(defaultWeights());
expect(params.alpha).toEqual(DEFAULT_ALPHA);
expect(params.intervalDecay).toEqual(0.1);
});
});
}); });

View File

@ -21,10 +21,12 @@ import {
paramsToJSON, paramsToJSON,
paramsFromJSON, paramsFromJSON,
type TimelineCredParametersJSON, type TimelineCredParametersJSON,
type PartialTimelineCredParameters,
partialParams,
defaultParams,
} from "./params"; } from "./params";
export type {Interval} from "./interval"; export type {Interval} from "./interval";
export type {TimelineCredParameters} from "./params";
/** /**
* A Graph Node wrapped with cred information. * A Graph Node wrapped with cred information.
@ -85,8 +87,14 @@ export class TimelineCred {
* *
* This returns a new TimelineCred; it does not modify the existing one. * This returns a new TimelineCred; it does not modify the existing one.
*/ */
async reanalyze(newParams: TimelineCredParameters): Promise<TimelineCred> { async reanalyze(
return await TimelineCred.compute(this._graph, newParams, this._plugins); newParams: PartialTimelineCredParameters
): Promise<TimelineCred> {
return await TimelineCred.compute({
graph: this._graph,
params: newParams,
plugins: this._plugins,
});
} }
/** /**
@ -217,11 +225,13 @@ export class TimelineCred {
return new TimelineCred(graph, intervalsJSON, cred, params, pluginsJSON); return new TimelineCred(graph, intervalsJSON, cred, params, pluginsJSON);
} }
static async compute( static async compute(opts: {|
graph: Graph, graph: Graph,
params: TimelineCredParameters, params?: PartialTimelineCredParameters,
plugins: $ReadOnlyArray<PluginDeclaration> plugins: $ReadOnlyArray<PluginDeclaration>,
): Promise<TimelineCred> { |}): Promise<TimelineCred> {
const {graph, params, plugins} = opts;
const fullParams = params == null ? defaultParams() : partialParams(params);
const nodeOrder = Array.from(graph.nodes()).map((x) => x.address); const nodeOrder = Array.from(graph.nodes()).map((x) => x.address);
const types = combineTypes(plugins); const types = combineTypes(plugins);
const userTypes = [].concat(...plugins.map((x) => x.userTypes)); const userTypes = [].concat(...plugins.map((x) => x.userTypes));
@ -229,9 +239,9 @@ export class TimelineCred {
const distribution = await timelinePagerank( const distribution = await timelinePagerank(
graph, graph,
types, types,
params.weights, fullParams.weights,
params.intervalDecay, fullParams.intervalDecay,
params.alpha fullParams.alpha
); );
const cred = distributionToCred( const cred = distributionToCred(
distribution, distribution,
@ -249,7 +259,7 @@ export class TimelineCred {
graph, graph,
intervals, intervals,
addressToCred, addressToCred,
params, fullParams,
plugins plugins
); );
return preliminaryCred.reduceSize({ return preliminaryCred.reduceSize({

View File

@ -11,8 +11,8 @@ import {
EdgeAddress, EdgeAddress,
} from "../../core/graph"; } from "../../core/graph";
import {TimelineCred} from "./timelineCred"; import {TimelineCred} from "./timelineCred";
import {defaultParams} from "./params";
import {type PluginDeclaration} from "../pluginDeclaration"; import {type PluginDeclaration} from "../pluginDeclaration";
import {defaultWeights} from "../weights";
import {type NodeType} from "../types"; import {type NodeType} from "../types";
describe("src/analysis/timeline/timelineCred", () => { describe("src/analysis/timeline/timelineCred", () => {
@ -84,8 +84,9 @@ describe("src/analysis/timeline/timelineCred", () => {
const scores = intervals.map((_) => i); const scores = intervals.map((_) => i);
addressToCred.set(address, scores); addressToCred.set(address, scores);
} }
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()}; return new TimelineCred(graph, intervals, addressToCred, defaultParams(), [
return new TimelineCred(graph, intervals, addressToCred, params, [plugin]); plugin,
]);
} }
it("JSON serialization works", () => { it("JSON serialization works", () => {

View File

@ -6,10 +6,9 @@ import path from "path";
import {TaskReporter} from "../util/taskReporter"; import {TaskReporter} from "../util/taskReporter";
import {Graph} from "../core/graph"; import {Graph} from "../core/graph";
import {loadGraph} from "../plugins/github/loadGraph"; import {loadGraph} from "../plugins/github/loadGraph";
import { import {TimelineCred} from "../analysis/timeline/timelineCred";
type TimelineCredParameters, import {defaultParams, partialParams} from "../analysis/timeline/params";
TimelineCred, import {type PartialTimelineCredParameters} from "../analysis/timeline/params";
} from "../analysis/timeline/timelineCred";
import {type Project} from "../core/project"; import {type Project} from "../core/project";
import {setupProjectDirectory} from "../core/project_io"; import {setupProjectDirectory} from "../core/project_io";
@ -19,7 +18,7 @@ import * as NullUtil from "../util/null";
export type LoadOptions = {| export type LoadOptions = {|
+project: Project, +project: Project,
+params: TimelineCredParameters, +params: ?PartialTimelineCredParameters,
+plugins: $ReadOnlyArray<PluginDeclaration>, +plugins: $ReadOnlyArray<PluginDeclaration>,
+sourcecredDirectory: string, +sourcecredDirectory: string,
+githubToken: string | null, +githubToken: string | null,
@ -45,6 +44,7 @@ export async function load(
taskReporter: TaskReporter taskReporter: TaskReporter
): Promise<void> { ): Promise<void> {
const {project, params, plugins, sourcecredDirectory, githubToken} = options; const {project, params, plugins, sourcecredDirectory, githubToken} = options;
const fullParams = params == null ? defaultParams() : partialParams(params);
const loadTask = `load-${options.project.id}`; const loadTask = `load-${options.project.id}`;
taskReporter.start(loadTask); taskReporter.start(loadTask);
const cacheDirectory = path.join(sourcecredDirectory, "cache"); const cacheDirectory = path.join(sourcecredDirectory, "cache");
@ -101,7 +101,7 @@ export async function load(
await fs.writeFile(graphFile, JSON.stringify(graph.toJSON())); await fs.writeFile(graphFile, JSON.stringify(graph.toJSON()));
taskReporter.start("compute-cred"); taskReporter.start("compute-cred");
const cred = await TimelineCred.compute(graph, params, plugins); const cred = await TimelineCred.compute({graph, params: fullParams, plugins});
const credJSON = cred.toJSON(); const credJSON = cred.toJSON();
const credFile = path.join(projectDirectory, "cred.json"); const credFile = path.join(projectDirectory, "cred.json");
await fs.writeFile(credFile, JSON.stringify(credJSON)); await fs.writeFile(credFile, JSON.stringify(credJSON));

View File

@ -19,6 +19,10 @@ import {NodeAddress, Graph} from "../core/graph";
import {node} from "../core/graphTestUtil"; import {node} from "../core/graphTestUtil";
import {TestTaskReporter} from "../util/taskReporter"; import {TestTaskReporter} from "../util/taskReporter";
import {load, type LoadOptions} from "./load"; import {load, type LoadOptions} from "./load";
import {
type PartialTimelineCredParameters,
partialParams,
} from "../analysis/timeline/params";
type JestMockFn = $Call<typeof jest.fn>; type JestMockFn = $Call<typeof jest.fn>;
jest.mock("../plugins/github/loadGraph", () => ({ jest.mock("../plugins/github/loadGraph", () => ({
@ -70,7 +74,7 @@ describe("api/load", () => {
// Tweaks the weights so that we can ensure we aren't overriding with default weights // Tweaks the weights so that we can ensure we aren't overriding with default weights
weights.nodeManualWeights.set(NodeAddress.empty, 33); weights.nodeManualWeights.set(NodeAddress.empty, 33);
// Deep freeze will freeze the weights, too // Deep freeze will freeze the weights, too
const params = deepFreeze({alpha: 0.05, intervalDecay: 0.5, weights}); const params: PartialTimelineCredParameters = {weights};
const plugins = deepFreeze([]); const plugins = deepFreeze([]);
const example = () => { const example = () => {
const sourcecredDirectory = tmp.dirSync().name; const sourcecredDirectory = tmp.dirSync().name;
@ -139,14 +143,10 @@ describe("api/load", () => {
it("calls TimelineCred.compute with the right graph and options", async () => { it("calls TimelineCred.compute with the right graph and options", async () => {
const {options, taskReporter} = example(); const {options, taskReporter} = example();
await load(options, taskReporter); await load(options, taskReporter);
expect(timelineCredCompute).toHaveBeenCalledWith( const args = timelineCredCompute.mock.calls[0][0];
expect.anything(), expect(args.graph.equals(combinedGraph())).toBe(true);
params, expect(args.params).toEqual(partialParams(params));
plugins expect(args.plugins).toEqual(plugins);
);
expect(timelineCredCompute.mock.calls[0][0].equals(combinedGraph())).toBe(
true
);
}); });
it("saves the resultant cred.json to disk", async () => { it("saves the resultant cred.json to disk", async () => {

View File

@ -9,6 +9,7 @@ import {defaultWeights, fromJSON as weightsFromJSON} from "../analysis/weights";
import {load} from "../api/load"; import {load} from "../api/load";
import {specToProject} from "../plugins/github/specToProject"; import {specToProject} from "../plugins/github/specToProject";
import fs from "fs-extra"; import fs from "fs-extra";
import {partialParams} from "../analysis/timeline/params";
import {DEFAULT_PLUGINS} from "./defaultPlugins"; import {DEFAULT_PLUGINS} from "./defaultPlugins";
function usage(print: (string) => void): void { function usage(print: (string) => void): void {
@ -105,7 +106,7 @@ const loadCommand: Command = async (args, std) => {
const projects = await Promise.all( const projects = await Promise.all(
projectSpecs.map((s) => specToProject(s, githubToken)) projectSpecs.map((s) => specToProject(s, githubToken))
); );
const params = {alpha: 0.05, intervalDecay: 0.5, weights}; const params = partialParams({weights});
const plugins = DEFAULT_PLUGINS; const plugins = DEFAULT_PLUGINS;
const optionses = projects.map((project) => ({ const optionses = projects.map((project) => ({
project, project,

View File

@ -11,6 +11,7 @@ import type {LoadOptions} from "../api/load";
import {defaultWeights, toJSON as weightsToJSON} from "../analysis/weights"; import {defaultWeights, toJSON as weightsToJSON} from "../analysis/weights";
import * as Common from "./common"; import * as Common from "./common";
import {DEFAULT_PLUGINS} from "./defaultPlugins"; import {DEFAULT_PLUGINS} from "./defaultPlugins";
import {defaultParams, partialParams} from "../analysis/timeline/params";
import {makeRepoId, stringToRepoId} from "../core/repoId"; import {makeRepoId, stringToRepoId} from "../core/repoId";
@ -78,7 +79,7 @@ describe("cli/load", () => {
discourseServer: null, discourseServer: null,
}, },
plugins: DEFAULT_PLUGINS, plugins: DEFAULT_PLUGINS,
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()}, params: defaultParams(),
sourcecredDirectory: Common.sourcecredDirectory(), sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken, githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey, discourseKey: fakeDiscourseKey,
@ -102,7 +103,7 @@ describe("cli/load", () => {
repoIds: [stringToRepoId(projectId)], repoIds: [stringToRepoId(projectId)],
discourseServer: null, discourseServer: null,
}, },
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()}, params: defaultParams(),
plugins: DEFAULT_PLUGINS, plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(), sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken, githubToken: fakeGithubToken,
@ -140,7 +141,7 @@ describe("cli/load", () => {
repoIds: [makeRepoId("foo", "bar")], repoIds: [makeRepoId("foo", "bar")],
discourseServer: null, discourseServer: null,
}, },
params: {alpha: 0.05, intervalDecay: 0.5, weights}, params: partialParams({weights}),
plugins: DEFAULT_PLUGINS, plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(), sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken, githubToken: fakeGithubToken,

View File

@ -6,7 +6,7 @@ import type {Assets} from "../webutil/assets";
import {TimelineCredView} from "./TimelineCredView"; import {TimelineCredView} from "./TimelineCredView";
import {Graph, NodeAddress} from "../core/graph"; import {Graph, NodeAddress} from "../core/graph";
import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred"; import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred";
import {defaultWeights} from "../analysis/weights"; import {defaultParams} from "../analysis/timeline/params";
export default class TimelineCredViewInspectiontest extends React.Component<{| export default class TimelineCredViewInspectiontest extends React.Component<{|
+assets: Assets, +assets: Assets,
@ -46,7 +46,7 @@ export default class TimelineCredViewInspectiontest extends React.Component<{|
const scores = intervals.map((_unuesd, i) => generator(i)); const scores = intervals.map((_unuesd, i) => generator(i));
addressToCred.set(address, scores); addressToCred.set(address, scores);
} }
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()}; const params = defaultParams();
return new TimelineCred(graph, intervals, addressToCred, params, []); return new TimelineCred(graph, intervals, addressToCred, params, []);
} }

View File

@ -4,10 +4,8 @@ import React from "react";
import deepEqual from "lodash.isequal"; import deepEqual from "lodash.isequal";
import {type Weights, copy as weightsCopy} from "../analysis/weights"; import {type Weights, copy as weightsCopy} from "../analysis/weights";
import {type NodeAddressT} from "../core/graph"; import {type NodeAddressT} from "../core/graph";
import { import {TimelineCred} from "../analysis/timeline/timelineCred";
TimelineCred, import {type TimelineCredParameters} from "../analysis/timeline/params";
type TimelineCredParameters,
} from "../analysis/timeline/timelineCred";
import {TimelineCredView} from "./TimelineCredView"; import {TimelineCredView} from "./TimelineCredView";
import Link from "../webutil/Link"; import Link from "../webutil/Link";
import {WeightConfig} from "./weights/WeightConfig"; import {WeightConfig} from "./weights/WeightConfig";