Backend: implement LoadContext (#1621)
Follows the general outline of #1586. It uses a new trick of aliasing external module functions as private properties. This makes the spyOn / mock tests more robust, while fitting in the composition responsibility.
This commit is contained in:
parent
5fd43bf62d
commit
f3cfab636e
|
@ -0,0 +1,125 @@
|
||||||
|
//@flow
|
||||||
|
|
||||||
|
import {type Project} from "../core/project";
|
||||||
|
import {type Weights as WeightsT} from "../core/weights";
|
||||||
|
import {type WeightedGraph as WeightedGraphT} from "../core/weightedGraph";
|
||||||
|
import * as WeightedGraph from "../core/weightedGraph";
|
||||||
|
import {type TimelineCredParameters} from "../analysis/timeline/params";
|
||||||
|
import {type GithubToken} from "../plugins/github/token";
|
||||||
|
import {type CacheProvider} from "./cache";
|
||||||
|
import {TaskReporter} from "../util/taskReporter";
|
||||||
|
import {TimelineCred} from "../analysis/timeline/timelineCred";
|
||||||
|
import {type ComputeFunction as ComputeFunctionT} from "./computeFunction";
|
||||||
|
import {type PluginLoaders as PluginLoadersT} from "./pluginLoaders";
|
||||||
|
import * as ComputeFunction from "./computeFunction";
|
||||||
|
import * as PluginLoaders from "./pluginLoaders";
|
||||||
|
import {default as githubLoader} from "../plugins/github/loader";
|
||||||
|
import {default as identityLoader} from "../plugins/identity/loader";
|
||||||
|
import {default as discourseLoader} from "../plugins/discourse/loader";
|
||||||
|
import {type PluginDeclarations} from "../analysis/pluginDeclaration";
|
||||||
|
|
||||||
|
export type LoadResult = {|
|
||||||
|
+pluginDeclarations: PluginDeclarations,
|
||||||
|
+weightedGraph: WeightedGraphT,
|
||||||
|
+cred: TimelineCred,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type LoadContextOptions = {|
|
||||||
|
+cache: CacheProvider,
|
||||||
|
+reporter: TaskReporter,
|
||||||
|
+githubToken: ?GithubToken,
|
||||||
|
|};
|
||||||
|
|
||||||
|
type OptionalLoadArguments = {|
|
||||||
|
+weightsOverrides?: WeightsT,
|
||||||
|
+params?: $Shape<TimelineCredParameters>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible composing all the variables and concrete functions
|
||||||
|
* of the loading process.
|
||||||
|
*
|
||||||
|
* Specifically it composes:
|
||||||
|
* - The loading environment (through the constructor).
|
||||||
|
* - Concrete functions of the loading process (internally).
|
||||||
|
* - Parameters for a load (Project and TimelineCredParameters).
|
||||||
|
*
|
||||||
|
* You can think of LoadContext as an instance where the environment and
|
||||||
|
* functions have been composed so it's ready to perform a load with.
|
||||||
|
*/
|
||||||
|
export class LoadContext {
|
||||||
|
+_options: LoadContextOptions;
|
||||||
|
|
||||||
|
constructor(opts: LoadContextOptions) {
|
||||||
|
this._options = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we're exposing multiple "proxy functions".
|
||||||
|
* They're just aliases of functions from another module. But by aliasing them
|
||||||
|
* as private properties we allow test code to spyOn these per LoadContext
|
||||||
|
* instance, and without needing to know details of the external modules.
|
||||||
|
*
|
||||||
|
* Of course this would break if the external module changes, but that would
|
||||||
|
* also occur if we directly depended on them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
+_declarations = PluginLoaders.declarations;
|
||||||
|
+_updateMirror = PluginLoaders.updateMirror;
|
||||||
|
+_createPluginGraphs = PluginLoaders.createPluginGraphs;
|
||||||
|
+_contractPluginGraphs = PluginLoaders.contractPluginGraphs;
|
||||||
|
+_overrideWeights = WeightedGraph.overrideWeights;
|
||||||
|
+_computeTask = ComputeFunction.computeTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The above proxy functions we're deferring to, accept interfaces so they
|
||||||
|
* could easily be mocked. This class takes the role of composing the concrete
|
||||||
|
* implementations though. So we're exposing them as aliases here, similar to
|
||||||
|
* the functions. As we'll need to test if these have been correctly passed on.
|
||||||
|
*/
|
||||||
|
|
||||||
|
+_compute: ComputeFunctionT = TimelineCred.compute;
|
||||||
|
+_pluginLoaders: PluginLoadersT = {
|
||||||
|
github: githubLoader,
|
||||||
|
discourse: discourseLoader,
|
||||||
|
identity: identityLoader,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a load in this context.
|
||||||
|
*/
|
||||||
|
async load(
|
||||||
|
project: Project,
|
||||||
|
{params, weightsOverrides}: OptionalLoadArguments
|
||||||
|
): Promise<LoadResult> {
|
||||||
|
const cachedProject = await this._updateMirror(
|
||||||
|
this._pluginLoaders,
|
||||||
|
this._options,
|
||||||
|
project
|
||||||
|
);
|
||||||
|
const pluginGraphs = await this._createPluginGraphs(
|
||||||
|
this._pluginLoaders,
|
||||||
|
this._options,
|
||||||
|
cachedProject
|
||||||
|
);
|
||||||
|
const contractedGraph = await this._contractPluginGraphs(
|
||||||
|
this._pluginLoaders,
|
||||||
|
pluginGraphs
|
||||||
|
);
|
||||||
|
let weightedGraph = contractedGraph;
|
||||||
|
if (weightsOverrides) {
|
||||||
|
weightedGraph = this._overrideWeights(contractedGraph, weightsOverrides);
|
||||||
|
}
|
||||||
|
const plugins = this._declarations(this._pluginLoaders, project);
|
||||||
|
const cred = await this._computeTask(this._compute, this._options, {
|
||||||
|
params,
|
||||||
|
plugins,
|
||||||
|
weightedGraph,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
pluginDeclarations: plugins,
|
||||||
|
weightedGraph,
|
||||||
|
cred,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {type CacheProvider} from "./cache";
|
||||||
|
import {type Project, createProject} from "../core/project";
|
||||||
|
import * as Weights from "../core/weights";
|
||||||
|
import {validateToken} from "../plugins/github/token";
|
||||||
|
import {TestTaskReporter} from "../util/taskReporter";
|
||||||
|
import {LoadContext} from "./loadContext";
|
||||||
|
|
||||||
|
const fakeDeclarations = (["fake-declaration"]: any);
|
||||||
|
const fakePluginGraphs = ({is: "fake-plugin-graphs"}: any);
|
||||||
|
const fakeContractedGraph = ({is: "fake-contracted-graph"}: any);
|
||||||
|
const fakeWeightedGraph = ({is: "fake-weighted-graph"}: any);
|
||||||
|
const fakeCred = ({is: "fake-cred"}: any);
|
||||||
|
|
||||||
|
const mockCache = (): CacheProvider => ({
|
||||||
|
database: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const calledMoreThanOnce = (name: string) => () => {
|
||||||
|
throw new Error(`Called ${name} more than once`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProxyMethods = (
|
||||||
|
loadContext: LoadContext,
|
||||||
|
project: Project,
|
||||||
|
cache: CacheProvider
|
||||||
|
) => ({
|
||||||
|
declarations: jest
|
||||||
|
.spyOn(loadContext, "_declarations")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_declarations"))
|
||||||
|
.mockReturnValueOnce(fakeDeclarations),
|
||||||
|
updateMirror: jest
|
||||||
|
.spyOn(loadContext, "_updateMirror")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_updateMirror"))
|
||||||
|
.mockResolvedValueOnce({project, cache}),
|
||||||
|
createPluginGraphs: jest
|
||||||
|
.spyOn(loadContext, "_createPluginGraphs")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_createPluginGraphs"))
|
||||||
|
.mockResolvedValueOnce(fakePluginGraphs),
|
||||||
|
contractPluginGraphs: jest
|
||||||
|
.spyOn(loadContext, "_contractPluginGraphs")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_contractPluginGraphs"))
|
||||||
|
.mockReturnValueOnce(fakeContractedGraph),
|
||||||
|
overrideWeights: jest
|
||||||
|
.spyOn(loadContext, "_overrideWeights")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_overrideWeights"))
|
||||||
|
.mockReturnValueOnce(fakeWeightedGraph),
|
||||||
|
computeTask: jest
|
||||||
|
.spyOn(loadContext, "_computeTask")
|
||||||
|
.mockImplementation(calledMoreThanOnce("_computeTask"))
|
||||||
|
.mockResolvedValueOnce(fakeCred),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("src/backend/loadContext", () => {
|
||||||
|
describe("LoadContext", () => {
|
||||||
|
const githubToken = validateToken("0".repeat(40));
|
||||||
|
const project = createProject({id: "testing-project"});
|
||||||
|
const params = {alpha: 0.123};
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
/**
|
||||||
|
* Note: we're not testing the proxy properties are the "correct" ones.
|
||||||
|
* This would be too constraining. Instead we should use an integration
|
||||||
|
* test to see if the results are as expected. However they should be
|
||||||
|
* exposed, in order to validate they are correctly called during `load`.
|
||||||
|
*/
|
||||||
|
it("should expose proxy properties", () => {
|
||||||
|
// Given
|
||||||
|
const cache = mockCache();
|
||||||
|
const reporter = new TestTaskReporter();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const loadContext = new LoadContext({cache, githubToken, reporter});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(loadContext).toMatchObject({
|
||||||
|
// Properties
|
||||||
|
_compute: expect.anything(),
|
||||||
|
_pluginLoaders: expect.anything(),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
_declarations: expect.anything(),
|
||||||
|
_updateMirror: expect.anything(),
|
||||||
|
_createPluginGraphs: expect.anything(),
|
||||||
|
_contractPluginGraphs: expect.anything(),
|
||||||
|
_overrideWeights: expect.anything(),
|
||||||
|
_computeTask: expect.anything(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("load", () => {
|
||||||
|
it("should call proxy methods with correct arguments", async () => {
|
||||||
|
// Given
|
||||||
|
const cache = mockCache();
|
||||||
|
const reporter = new TestTaskReporter();
|
||||||
|
const weightsOverrides = Weights.empty();
|
||||||
|
const loadContext = new LoadContext({cache, githubToken, reporter});
|
||||||
|
const spies = mockProxyMethods(loadContext, project, cache);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await loadContext.load(project, {weightsOverrides, params});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
const cachedProject = {project, cache};
|
||||||
|
const expectedEnv = {
|
||||||
|
githubToken,
|
||||||
|
reporter,
|
||||||
|
cache,
|
||||||
|
};
|
||||||
|
expect(spies.declarations).toBeCalledWith(
|
||||||
|
loadContext._pluginLoaders,
|
||||||
|
project
|
||||||
|
);
|
||||||
|
expect(spies.updateMirror).toBeCalledWith(
|
||||||
|
loadContext._pluginLoaders,
|
||||||
|
expectedEnv,
|
||||||
|
project
|
||||||
|
);
|
||||||
|
expect(spies.createPluginGraphs).toBeCalledWith(
|
||||||
|
loadContext._pluginLoaders,
|
||||||
|
expectedEnv,
|
||||||
|
cachedProject
|
||||||
|
);
|
||||||
|
expect(spies.contractPluginGraphs).toBeCalledWith(
|
||||||
|
loadContext._pluginLoaders,
|
||||||
|
fakePluginGraphs
|
||||||
|
);
|
||||||
|
expect(spies.overrideWeights).toBeCalledWith(
|
||||||
|
fakeContractedGraph,
|
||||||
|
weightsOverrides
|
||||||
|
);
|
||||||
|
expect(spies.computeTask).toBeCalledWith(
|
||||||
|
loadContext._compute,
|
||||||
|
expectedEnv,
|
||||||
|
{weightedGraph: fakeWeightedGraph, plugins: fakeDeclarations, params}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support omitting optional arguments", async () => {
|
||||||
|
// Given
|
||||||
|
const cache = mockCache();
|
||||||
|
const reporter = new TestTaskReporter();
|
||||||
|
const loadContext = new LoadContext({cache, githubToken, reporter});
|
||||||
|
const spies = mockProxyMethods(loadContext, project, cache);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await loadContext.load(project, {});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
const expectedEnv = {
|
||||||
|
githubToken,
|
||||||
|
reporter,
|
||||||
|
cache,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Omitting weight overrides option, should not call this function.
|
||||||
|
expect(spies.overrideWeights).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
// We have a different input graph, because weight overrides wasn't called.
|
||||||
|
// We're omitting the `params` argument from the options.
|
||||||
|
expect(spies.computeTask).toBeCalledWith(
|
||||||
|
loadContext._compute,
|
||||||
|
expectedEnv,
|
||||||
|
{weightedGraph: fakeContractedGraph, plugins: fakeDeclarations}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a LoadResult", async () => {
|
||||||
|
// Given
|
||||||
|
const cache = mockCache();
|
||||||
|
const reporter = new TestTaskReporter();
|
||||||
|
const weightsOverrides = Weights.empty();
|
||||||
|
const loadContext = new LoadContext({cache, githubToken, reporter});
|
||||||
|
mockProxyMethods(loadContext, project, cache);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await loadContext.load(project, {
|
||||||
|
weightsOverrides,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toEqual({
|
||||||
|
pluginDeclarations: fakeDeclarations,
|
||||||
|
weightedGraph: fakeWeightedGraph,
|
||||||
|
cred: fakeCred,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue