legacy app state includes TimelineCred (#1428)

By keeping the TimelineCred in state instead of the Graph, we can access
the plugin information (and potentially other config) from TimelineCred.
Note that the legacy app does still use old-style cred calculation (no
time weighting).

Test plan: `yarn test`. It's just a refactor.

Part of https://discourse.sourcecred.io/t/fixup-legacy-explorer/316
This commit is contained in:
Dandelion Mané 2019-10-28 23:49:11 -06:00 committed by GitHub
parent d896f73329
commit 3754cafb7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 74 deletions

View File

@ -121,7 +121,7 @@ export function createApp(
weightFileManager={weightFileManager}
manualWeights={this.state.weights.nodeManualWeights}
declarations={[githubDeclaration]}
graph={appState.graph}
graph={appState.timelineCred.graph()}
onManualWeightsChange={(addr: NodeAddressT, weight: number) =>
this.setState(({weights}) => {
weights.nodeManualWeights.set(addr, weight);
@ -156,7 +156,7 @@ export function createApp(
appState.loading === "LOADING"
}
onClick={() =>
this.stateTransitionMachine.loadGraphAndRunPagerank(
this.stateTransitionMachine.loadTimelineCredAndRunPagerank(
this.props.assets,
this.state.weights,
{

View File

@ -9,23 +9,25 @@ import testLocalStore from "../../webutil/testLocalStore";
import {PagerankTable} from "./pagerankTable/Table";
import {createApp, LoadingIndicator, ProjectDetail} from "./App";
import {TimelineCred} from "../../analysis/timeline/timelineCred";
import {defaultParams} from "../../analysis/timeline/params";
require("../../webutil/testUtil").configureEnzyme();
describe("explorer/legacy/App", () => {
function example() {
let setState, getState;
const loadGraph = jest.fn();
const loadTimelineCred = jest.fn();
const runPagerank = jest.fn();
const loadGraphAndRunPagerank = jest.fn();
const loadTimelineCredAndRunPagerank = jest.fn();
const localStore = testLocalStore();
function createMockSTM(_getState, _setState) {
setState = _setState;
getState = _getState;
return {
loadGraph,
loadTimelineCred,
runPagerank,
loadGraphAndRunPagerank,
loadTimelineCredAndRunPagerank,
};
}
const App = createApp(createMockSTM);
@ -43,9 +45,9 @@ describe("explorer/legacy/App", () => {
el,
setState,
getState,
loadGraph,
loadTimelineCred,
runPagerank,
loadGraphAndRunPagerank,
loadTimelineCredAndRunPagerank,
localStore,
};
}
@ -63,7 +65,13 @@ describe("explorer/legacy/App", () => {
type: "READY_TO_RUN_PAGERANK",
projectId: "foo/bar",
loading: loadingState,
graph: new Graph(),
timelineCred: new TimelineCred(
new Graph(),
[],
new Map(),
defaultParams(),
[]
),
});
},
pagerankEvaluated: (loadingState) => {
@ -71,7 +79,13 @@ describe("explorer/legacy/App", () => {
type: "PAGERANK_EVALUATED",
projectId: "foo/bar",
loading: loadingState,
graph: new Graph(),
timelineCred: new TimelineCred(
new Graph(),
[],
new Map(),
defaultParams(),
[]
),
pagerankNodeDecomposition: new Map(),
});
},
@ -123,7 +137,7 @@ describe("explorer/legacy/App", () => {
function testAnalyzeCredButton(stateFn, {disabled}) {
const adjective = disabled ? "disabled" : "working";
it(`has a ${adjective} analyze cred button`, () => {
const {el, loadGraphAndRunPagerank, setState} = example();
const {el, loadTimelineCredAndRunPagerank, setState} = example();
setState(stateFn());
el.update();
const button = el.findWhere(
@ -134,7 +148,7 @@ describe("explorer/legacy/App", () => {
} else {
expect(button.props().disabled).toBe(false);
button.simulate("click");
expect(loadGraphAndRunPagerank).toBeCalledTimes(1);
expect(loadTimelineCredAndRunPagerank).toBeCalledTimes(1);
}
});
}

View File

@ -12,6 +12,7 @@ import {
type PagerankOptions,
pagerank,
} from "../../analysis/pagerank";
import {TimelineCred} from "../../analysis/timeline/timelineCred";
import type {Weights} from "../../analysis/weights";
import {weightsToEdgeEvaluator} from "../../analysis/weightsToEdgeEvaluator";
@ -38,12 +39,12 @@ export type ReadyToLoadGraph = {|
export type ReadyToRunPagerank = {|
+type: "READY_TO_RUN_PAGERANK",
+projectId: string,
+graph: Graph,
+timelineCred: TimelineCred,
+loading: LoadingState,
|};
export type PagerankEvaluated = {|
+type: "PAGERANK_EVALUATED",
+graph: Graph,
+timelineCred: TimelineCred,
+projectId: string,
+pagerankNodeDecomposition: PagerankNodeDecomposition,
+loading: LoadingState,
@ -57,14 +58,19 @@ export function createStateTransitionMachine(
getState: () => AppState,
setState: (AppState) => void
): StateTransitionMachine {
return new StateTransitionMachine(getState, setState, doLoadGraph, pagerank);
return new StateTransitionMachine(
getState,
setState,
doLoadTimelineCred,
pagerank
);
}
// Exported for testing purposes.
export interface StateTransitionMachineInterface {
+loadGraph: (Assets) => Promise<boolean>;
+loadTimelineCred: (Assets) => Promise<boolean>;
+runPagerank: (Weights, NodeAndEdgeTypes, NodeAddressT) => Promise<void>;
+loadGraphAndRunPagerank: (
+loadTimelineCredAndRunPagerank: (
Assets,
Weights,
NodeAndEdgeTypes,
@ -72,13 +78,16 @@ export interface StateTransitionMachineInterface {
) => Promise<void>;
}
/* In production, instantiate via createStateTransitionMachine; the constructor
* implementation allows specification of the loadGraph and
* implementation allows specification of the loadTimelineCred and
* pagerank functions for DI/testing purposes.
**/
export class StateTransitionMachine implements StateTransitionMachineInterface {
getState: () => AppState;
setState: (AppState) => void;
doLoadGraph: (assets: Assets, projectId: string) => Promise<Graph>;
doLoadTimelineCred: (
assets: Assets,
projectId: string
) => Promise<TimelineCred>;
pagerank: (
Graph,
EdgeEvaluator,
@ -88,7 +97,10 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
constructor(
getState: () => AppState,
setState: (AppState) => void,
doLoadGraph: (assets: Assets, projectId: string) => Promise<Graph>,
doLoadTimelineCred: (
assets: Assets,
projectId: string
) => Promise<TimelineCred>,
pagerank: (
Graph,
EdgeEvaluator,
@ -97,15 +109,15 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
) {
this.getState = getState;
this.setState = setState;
this.doLoadGraph = doLoadGraph;
this.doLoadTimelineCred = doLoadTimelineCred;
this.pagerank = pagerank;
}
/** Loads the graph, reports whether it was successful */
async loadGraph(assets: Assets): Promise<boolean> {
async loadTimelineCred(assets: Assets): Promise<boolean> {
const state = this.getState();
if (state.type !== "READY_TO_LOAD_GRAPH") {
throw new Error("Tried to loadGraph in incorrect state");
throw new Error("Tried to loadTimelineCred in incorrect state");
}
const {projectId} = state;
const loadingState = {...state, loading: "LOADING"};
@ -113,10 +125,10 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
let newState: ?AppState;
let success = true;
try {
const graph = await this.doLoadGraph(assets, projectId);
const timelineCred = await this.doLoadTimelineCred(assets, projectId);
newState = {
type: "READY_TO_RUN_PAGERANK",
graph,
timelineCred,
projectId,
loading: "NOT_LOADING",
};
@ -150,7 +162,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
? {...state, loading: "LOADING"}
: {...state, loading: "LOADING"};
this.setState(loadingState);
const graph = state.graph;
const graph = state.timelineCred.graph();
let newState: ?AppState;
try {
const pagerankNodeDecomposition = await this.pagerank(
@ -164,7 +176,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
newState = {
type: "PAGERANK_EVALUATED",
pagerankNodeDecomposition,
graph: state.graph,
timelineCred: state.timelineCred,
projectId: state.projectId,
loading: "NOT_LOADING",
};
@ -181,7 +193,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
}
}
async loadGraphAndRunPagerank(
async loadTimelineCredAndRunPagerank(
assets: Assets,
weights: Weights,
types: NodeAndEdgeTypes,
@ -191,8 +203,8 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
const type = state.type;
switch (type) {
case "READY_TO_LOAD_GRAPH":
const loadedGraph = await this.loadGraph(assets);
if (loadedGraph) {
const loadedTimelineCred = await this.loadTimelineCred(assets);
if (loadedTimelineCred) {
await this.runPagerank(weights, types, totalScoreNodePrefix);
}
break;
@ -206,13 +218,13 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
}
}
export async function doLoadGraph(
export async function doLoadTimelineCred(
assets: Assets,
projectId: string
): Promise<Graph> {
): Promise<TimelineCred> {
const loadResult = await defaultLoader(assets, projectId);
if (loadResult.type !== "SUCCESS") {
throw new Error(loadResult);
}
return loadResult.timelineCred.graph();
return loadResult.timelineCred;
}

View File

@ -10,6 +10,8 @@ import type {
PagerankNodeDecomposition,
PagerankOptions,
} from "../../analysis/pagerank";
import {TimelineCred} from "../../analysis/timeline/timelineCred";
import {defaultParams} from "../../analysis/timeline/params";
describe("explorer/legacy/state", () => {
function example(startingState: AppState) {
@ -18,9 +20,9 @@ describe("explorer/legacy/state", () => {
const setState = (appState) => {
stateContainer.appState = appState;
};
const loadGraphMock: JestMockFn<
const loadTimelineCredMock: JestMockFn<
[Assets, string],
Promise<Graph>
Promise<TimelineCred>
> = jest.fn();
const pagerankMock: JestMockFn<
@ -30,10 +32,10 @@ describe("explorer/legacy/state", () => {
const stm = new StateTransitionMachine(
getState,
setState,
loadGraphMock,
loadTimelineCredMock,
pagerankMock
);
return {getState, stm, loadGraphMock, pagerankMock};
return {getState, stm, loadTimelineCredMock, pagerankMock};
}
function readyToLoadGraph(): AppState {
return {
@ -47,14 +49,26 @@ describe("explorer/legacy/state", () => {
type: "READY_TO_RUN_PAGERANK",
projectId: "foo/bar",
loading: "NOT_LOADING",
graph: new Graph(),
timelineCred: new TimelineCred(
new Graph(),
[],
new Map(),
defaultParams(),
[]
),
};
}
function pagerankEvaluated(): AppState {
return {
type: "PAGERANK_EVALUATED",
projectId: "foo/bar",
graph: new Graph(),
timelineCred: new TimelineCred(
new Graph(),
[],
new Map(),
defaultParams(),
[]
),
pagerankNodeDecomposition: pagerankNodeDecomposition(),
loading: "NOT_LOADING",
};
@ -69,36 +83,43 @@ describe("explorer/legacy/state", () => {
return state.loading;
}
describe("loadGraph", () => {
describe("loadTimelineCred", () => {
it("can only be called when READY_TO_LOAD_GRAPH", async () => {
const badStates = [readyToRunPagerank(), pagerankEvaluated()];
for (const b of badStates) {
const {stm} = example(b);
await expect(stm.loadGraph(new Assets("/my/gateway/"))).rejects.toThrow(
"incorrect state"
);
await expect(
stm.loadTimelineCred(new Assets("/my/gateway/"))
).rejects.toThrow("incorrect state");
}
});
it("passes along the projectId", () => {
const {stm, loadGraphMock} = example(readyToLoadGraph());
expect(loadGraphMock).toHaveBeenCalledTimes(0);
const {stm, loadTimelineCredMock} = example(readyToLoadGraph());
expect(loadTimelineCredMock).toHaveBeenCalledTimes(0);
const assets = new Assets("/my/gateway/");
stm.loadGraph(assets);
expect(loadGraphMock).toHaveBeenCalledTimes(1);
expect(loadGraphMock).toHaveBeenCalledWith(assets, "foo/bar");
stm.loadTimelineCred(assets);
expect(loadTimelineCredMock).toHaveBeenCalledTimes(1);
expect(loadTimelineCredMock).toHaveBeenCalledWith(assets, "foo/bar");
});
it("immediately sets loading status", () => {
const {getState, stm} = example(readyToLoadGraph());
expect(loading(getState())).toBe("NOT_LOADING");
stm.loadGraph(new Assets("/my/gateway/"));
stm.loadTimelineCred(new Assets("/my/gateway/"));
expect(loading(getState())).toBe("LOADING");
expect(getState().type).toBe("READY_TO_LOAD_GRAPH");
});
it("transitions to READY_TO_RUN_PAGERANK on success", async () => {
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
const graph = new Graph();
loadGraphMock.mockReturnValue(Promise.resolve(graph));
const succeeded = await stm.loadGraph(new Assets("/my/gateway/"));
const {getState, stm, loadTimelineCredMock} = example(readyToLoadGraph());
const timelineCred = new TimelineCred(
new Graph(),
[],
new Map(),
defaultParams(),
[]
);
loadTimelineCredMock.mockReturnValue(Promise.resolve(timelineCred));
const succeeded = await stm.loadTimelineCred(new Assets("/my/gateway/"));
expect(succeeded).toBe(true);
const state = getState();
expect(loading(state)).toBe("NOT_LOADING");
@ -106,15 +127,15 @@ describe("explorer/legacy/state", () => {
if (state.type !== "READY_TO_RUN_PAGERANK") {
throw new Error("Impossible");
}
expect(state.graph).toBe(graph);
expect(state.timelineCred).toBe(timelineCred);
});
it("sets loading state FAILED on reject", async () => {
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
const {getState, stm, loadTimelineCredMock} = example(readyToLoadGraph());
const error = new Error("Oh no!");
// $ExpectFlowError
console.error = jest.fn();
loadGraphMock.mockReturnValue(Promise.reject(error));
const succeeded = await stm.loadGraph(new Assets("/my/gateway/"));
loadTimelineCredMock.mockReturnValue(Promise.reject(error));
const succeeded = await stm.loadTimelineCred(new Assets("/my/gateway/"));
expect(succeeded).toBe(false);
const state = getState();
expect(loading(state)).toBe("FAILED");
@ -183,69 +204,69 @@ describe("explorer/legacy/state", () => {
});
});
describe("loadGraphAndRunPagerank", () => {
describe("loadTimelineCredAndRunPagerank", () => {
it("when READY_TO_LOAD_GRAPH, loads graph then runs pagerank", async () => {
const {stm} = example(readyToLoadGraph());
(stm: any).loadGraph = jest.fn();
(stm: any).loadTimelineCred = jest.fn();
(stm: any).runPagerank = jest.fn();
stm.loadGraph.mockResolvedValue(true);
stm.loadTimelineCred.mockResolvedValue(true);
const assets = new Assets("/gateway/");
const prefix = NodeAddress.fromParts(["bar"]);
const types = defaultTypes();
const wt = defaultWeights();
await stm.loadGraphAndRunPagerank(assets, wt, types, prefix);
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
expect(stm.loadGraph).toHaveBeenCalledWith(assets);
await stm.loadTimelineCredAndRunPagerank(assets, wt, types, prefix);
expect(stm.loadTimelineCred).toHaveBeenCalledTimes(1);
expect(stm.loadTimelineCred).toHaveBeenCalledWith(assets);
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, types, prefix);
});
it("does not run pagerank if loadGraph did not succeed", async () => {
it("does not run pagerank if loadTimelineCred did not succeed", async () => {
const {stm} = example(readyToLoadGraph());
(stm: any).loadGraph = jest.fn();
(stm: any).loadTimelineCred = jest.fn();
(stm: any).runPagerank = jest.fn();
stm.loadGraph.mockResolvedValue(false);
stm.loadTimelineCred.mockResolvedValue(false);
const assets = new Assets("/gateway/");
const prefix = NodeAddress.fromParts(["bar"]);
await stm.loadGraphAndRunPagerank(
await stm.loadTimelineCredAndRunPagerank(
assets,
defaultWeights(),
defaultTypes(),
prefix
);
expect(stm.loadGraph).toHaveBeenCalledTimes(1);
expect(stm.loadTimelineCred).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledTimes(0);
});
it("when READY_TO_RUN_PAGERANK, runs pagerank", async () => {
const {stm} = example(readyToRunPagerank());
(stm: any).loadGraph = jest.fn();
(stm: any).loadTimelineCred = jest.fn();
(stm: any).runPagerank = jest.fn();
const prefix = NodeAddress.fromParts(["bar"]);
const wt = defaultWeights();
const types = defaultTypes();
await stm.loadGraphAndRunPagerank(
await stm.loadTimelineCredAndRunPagerank(
new Assets("/gateway/"),
wt,
types,
prefix
);
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
expect(stm.loadTimelineCred).toHaveBeenCalledTimes(0);
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, types, prefix);
});
it("when PAGERANK_EVALUATED, runs pagerank", async () => {
const {stm} = example(pagerankEvaluated());
(stm: any).loadGraph = jest.fn();
(stm: any).loadTimelineCred = jest.fn();
(stm: any).runPagerank = jest.fn();
const prefix = NodeAddress.fromParts(["bar"]);
const wt = defaultWeights();
const types = defaultTypes();
await stm.loadGraphAndRunPagerank(
await stm.loadTimelineCredAndRunPagerank(
new Assets("/gateway/"),
wt,
types,
prefix
);
expect(stm.loadGraph).toHaveBeenCalledTimes(0);
expect(stm.loadTimelineCred).toHaveBeenCalledTimes(0);
expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, types, prefix);
});