Refactor graph-related test code (#1179)
This commit adds new helper methods for creating test nodes (`node` and `partsNode`) and for creating test edges (`edge` and `partsEdge`) to graphTestUtil.js. This is very helpful in light of work related to #1136. I'm going to change the concept of "node" from a raw address to an object, add fields to that object, and add fields to the `Edge` type. If done naively, we would need to change all the test code across the project for every one of those changes. By centralizing the creation of test nodes and edges behind the new functions, we can update all the test code in a single place. This change is trivial from a conceputal perspective, and very broad-reaching from a code-touching perspective. It should be easy to review, because if tests pass then the change is probably working as intended. :) Test plan: `yarn test`
This commit is contained in:
parent
e916bc91c8
commit
e493af2307
|
@ -16,6 +16,7 @@ import type {
|
||||||
import * as RepoIdRegistry from "../core/repoIdRegistry";
|
import * as RepoIdRegistry from "../core/repoIdRegistry";
|
||||||
import {makeRepoId, type RepoId} from "../core/repoId";
|
import {makeRepoId, type RepoId} from "../core/repoId";
|
||||||
import {loadGraph} from "./loadGraph";
|
import {loadGraph} from "./loadGraph";
|
||||||
|
import {node} from "../core/graphTestUtil";
|
||||||
|
|
||||||
const declaration = (name) => ({
|
const declaration = (name) => ({
|
||||||
name,
|
name,
|
||||||
|
@ -115,8 +116,8 @@ describe("analysis/loadGraph", () => {
|
||||||
expect(result).toEqual({status: "REPO_NOT_LOADED"});
|
expect(result).toEqual({status: "REPO_NOT_LOADED"});
|
||||||
});
|
});
|
||||||
it("returns status:SUCCESS with merged graph on success", async () => {
|
it("returns status:SUCCESS with merged graph on success", async () => {
|
||||||
const g1 = new Graph().addNode(NodeAddress.fromParts(["g1"]));
|
const g1 = new Graph().addNode(node("n1"));
|
||||||
const g2 = new Graph().addNode(NodeAddress.fromParts(["g2"]));
|
const g2 = new Graph().addNode(node("n2"));
|
||||||
const m1 = new MockStaticAdapter("foo", g1);
|
const m1 = new MockStaticAdapter("foo", g1);
|
||||||
const m2 = new MockStaticAdapter("bar", g2);
|
const m2 = new MockStaticAdapter("bar", g2);
|
||||||
const mergedGraph = Graph.merge([g1, g2]);
|
const mergedGraph = Graph.merge([g1, g2]);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import {EdgeAddress, Graph, NodeAddress, edgeToStrings} from "../core/graph";
|
import {Graph, NodeAddress, edgeToStrings} from "../core/graph";
|
||||||
import {
|
import {
|
||||||
distributionToNodeDistribution,
|
distributionToNodeDistribution,
|
||||||
createConnections,
|
createConnections,
|
||||||
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "./pagerankNodeDecomposition";
|
} from "./pagerankNodeDecomposition";
|
||||||
import * as MapUtil from "../util/map";
|
import * as MapUtil from "../util/map";
|
||||||
|
|
||||||
import {advancedGraph} from "../core/graphTestUtil";
|
import {advancedGraph, node, edge} from "../core/graphTestUtil";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a decomposition to be shown in a snapshot. This converts
|
* Format a decomposition to be shown in a snapshot. This converts
|
||||||
|
@ -117,13 +117,13 @@ function validateDecomposition(decomposition) {
|
||||||
describe("analysis/pagerankNodeDecomposition", () => {
|
describe("analysis/pagerankNodeDecomposition", () => {
|
||||||
describe("decompose", () => {
|
describe("decompose", () => {
|
||||||
it("has the expected output on a simple asymmetric chain", async () => {
|
it("has the expected output on a simple asymmetric chain", async () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const n1 = node("n1");
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
const n2 = node("n2");
|
||||||
const n3 = NodeAddress.fromParts(["sink"]);
|
const n3 = node("sink");
|
||||||
const e1 = {src: n1, dst: n2, address: EdgeAddress.fromParts(["e1"])};
|
const e1 = edge("e1", n1, n2);
|
||||||
const e2 = {src: n2, dst: n3, address: EdgeAddress.fromParts(["e2"])};
|
const e2 = edge("e2", n2, n3);
|
||||||
const e3 = {src: n1, dst: n3, address: EdgeAddress.fromParts(["e3"])};
|
const e3 = edge("e3", n1, n3);
|
||||||
const e4 = {src: n3, dst: n3, address: EdgeAddress.fromParts(["e4"])};
|
const e4 = edge("e4", n3, n3);
|
||||||
const g = new Graph()
|
const g = new Graph()
|
||||||
.addNode(n1)
|
.addNode(n1)
|
||||||
.addNode(n2)
|
.addNode(n2)
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import {run} from "./testUtil";
|
import {run} from "./testUtil";
|
||||||
import {help, makeExportGraph} from "./exportGraph";
|
import {help, makeExportGraph} from "./exportGraph";
|
||||||
import {Graph, NodeAddress} from "../core/graph";
|
import {Graph} from "../core/graph";
|
||||||
|
import {node} from "../core/graphTestUtil";
|
||||||
import stringify from "json-stable-stringify";
|
import stringify from "json-stable-stringify";
|
||||||
|
|
||||||
import {makeRepoId} from "../core/repoId";
|
import {makeRepoId} from "../core/repoId";
|
||||||
|
@ -61,7 +62,7 @@ describe("cli/exportGraph", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("on load success, prints the stringified graph to stdout", async () => {
|
it("on load success, prints the stringified graph to stdout", async () => {
|
||||||
const graph = new Graph().addNode(NodeAddress.empty);
|
const graph = new Graph().addNode(node("n"));
|
||||||
const loadGraphResult = {status: "SUCCESS", graph};
|
const loadGraphResult = {status: "SUCCESS", graph};
|
||||||
const exportGraph = makeExportGraph(
|
const exportGraph = makeExportGraph(
|
||||||
(_unused_repoId) => new Promise((resolve) => resolve(loadGraphResult))
|
(_unused_repoId) => new Promise((resolve) => resolve(loadGraphResult))
|
||||||
|
|
|
@ -314,7 +314,8 @@ export async function saveTimestamps(
|
||||||
const adapters = await Promise.all(
|
const adapters = await Promise.all(
|
||||||
adapterLoaders.map((a) => a.load(Common.sourcecredDirectory(), repoId))
|
adapterLoaders.map((a) => a.load(Common.sourcecredDirectory(), repoId))
|
||||||
);
|
);
|
||||||
const timestampMap = createTimestampMap(graph.nodes(), adapters);
|
const nodeAddresses = Array.from(graph.nodes());
|
||||||
|
const timestampMap = createTimestampMap(nodeAddresses, adapters);
|
||||||
writeTimestampMap(timestampMap, Common.sourcecredDirectory(), repoId);
|
writeTimestampMap(timestampMap, Common.sourcecredDirectory(), repoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
defaultSaver,
|
defaultSaver,
|
||||||
} from "./pagerank";
|
} from "./pagerank";
|
||||||
import {Graph, NodeAddress, EdgeAddress} from "../core/graph";
|
import {Graph, NodeAddress, EdgeAddress} from "../core/graph";
|
||||||
import {advancedGraph} from "../core/graphTestUtil";
|
import {node, edge, advancedGraph} from "../core/graphTestUtil";
|
||||||
import {
|
import {
|
||||||
PagerankGraph,
|
PagerankGraph,
|
||||||
DEFAULT_SYNTHETIC_LOOP_WEIGHT,
|
DEFAULT_SYNTHETIC_LOOP_WEIGHT,
|
||||||
|
@ -127,7 +127,7 @@ describe("cli/pagerank", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("on successful load", () => {
|
describe("on successful load", () => {
|
||||||
const graph = () => new Graph().addNode(NodeAddress.empty);
|
const graph = () => new Graph().addNode(node("n"));
|
||||||
const graphResult = () => ({status: "SUCCESS", graph: graph()});
|
const graphResult = () => ({status: "SUCCESS", graph: graph()});
|
||||||
const loader = (_unused_repoId) =>
|
const loader = (_unused_repoId) =>
|
||||||
new Promise((resolve) => resolve(graphResult()));
|
new Promise((resolve) => resolve(graphResult()));
|
||||||
|
@ -169,7 +169,7 @@ describe("cli/pagerank", () => {
|
||||||
|
|
||||||
describe("savePagerankGraph", () => {
|
describe("savePagerankGraph", () => {
|
||||||
it("saves the PagerankGraphJSON to the right filepath", async () => {
|
it("saves the PagerankGraphJSON to the right filepath", async () => {
|
||||||
const graph = new Graph().addNode(NodeAddress.empty);
|
const graph = new Graph().addNode(node("n"));
|
||||||
const evaluator = (_unused_edge) => ({toWeight: 1, froWeight: 2});
|
const evaluator = (_unused_edge) => ({toWeight: 1, froWeight: 2});
|
||||||
const prg = new PagerankGraph(graph, evaluator);
|
const prg = new PagerankGraph(graph, evaluator);
|
||||||
const dirname = tmp.dirSync().name;
|
const dirname = tmp.dirSync().name;
|
||||||
|
@ -229,11 +229,9 @@ describe("cli/pagerank", () => {
|
||||||
expect(actualPagerankGraph.equals(expectedPagerankGraph)).toBe(true);
|
expect(actualPagerankGraph.equals(expectedPagerankGraph)).toBe(true);
|
||||||
});
|
});
|
||||||
it("default pageRank is robust to nodes that are not owned by any plugin", async () => {
|
it("default pageRank is robust to nodes that are not owned by any plugin", async () => {
|
||||||
const graph = new Graph().addNode(NodeAddress.empty).addEdge({
|
const n = node("n");
|
||||||
address: EdgeAddress.empty,
|
const e = edge("no-plugin", n, n);
|
||||||
src: NodeAddress.empty,
|
const graph = new Graph().addNode(n).addEdge(e);
|
||||||
dst: NodeAddress.empty,
|
|
||||||
});
|
|
||||||
await defaultPagerank(graph);
|
await defaultPagerank(graph);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -242,8 +240,11 @@ describe("cli/pagerank", () => {
|
||||||
process.env.SOURCECRED_DIRECTORY = dirname;
|
process.env.SOURCECRED_DIRECTORY = dirname;
|
||||||
const repoId = makeRepoId("foo", "bar");
|
const repoId = makeRepoId("foo", "bar");
|
||||||
const prg = new PagerankGraph(
|
const prg = new PagerankGraph(
|
||||||
new Graph().addNode(NodeAddress.empty),
|
new Graph().addNode(node("n")),
|
||||||
(_unused_edge) => ({toWeight: 1, froWeight: 2})
|
(_unused_edge) => ({
|
||||||
|
toWeight: 1,
|
||||||
|
froWeight: 2,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
await defaultSaver(repoId, prg);
|
await defaultSaver(repoId, prg);
|
||||||
const expectedPath = path.join(
|
const expectedPath = path.join(
|
||||||
|
|
|
@ -17,11 +17,10 @@ Array [
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"address": Array [
|
"address": Array [
|
||||||
"edge",
|
"wat",
|
||||||
"2",
|
|
||||||
],
|
],
|
||||||
"dstIndex": 1,
|
"dstIndex": 0,
|
||||||
"srcIndex": 0,
|
"srcIndex": 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": Array [
|
"nodes": Array [
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import sortBy from "lodash.sortby";
|
import sortBy from "lodash.sortby";
|
||||||
|
|
||||||
import {EdgeAddress, Graph, NodeAddress} from "../graph";
|
import {Graph, NodeAddress} from "../graph";
|
||||||
import {
|
import {
|
||||||
distributionToNodeDistribution,
|
distributionToNodeDistribution,
|
||||||
createConnections,
|
createConnections,
|
||||||
|
@ -14,13 +14,13 @@ import {
|
||||||
} from "./graphToMarkovChain";
|
} from "./graphToMarkovChain";
|
||||||
import * as MapUtil from "../../util/map";
|
import * as MapUtil from "../../util/map";
|
||||||
|
|
||||||
import {advancedGraph} from "../graphTestUtil";
|
import {node, advancedGraph, edge} from "../graphTestUtil";
|
||||||
|
|
||||||
describe("core/attribution/graphToMarkovChain", () => {
|
describe("core/attribution/graphToMarkovChain", () => {
|
||||||
|
const n1 = node("n1");
|
||||||
|
const n2 = node("n2");
|
||||||
|
const n3 = node("n3");
|
||||||
describe("permute", () => {
|
describe("permute", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
|
||||||
const n3 = NodeAddress.fromParts(["n3"]);
|
|
||||||
// This chain isn't a proper stochastic chain, but that's okay:
|
// This chain isn't a proper stochastic chain, but that's okay:
|
||||||
// the actual values aren't relevant.
|
// the actual values aren't relevant.
|
||||||
const old = {
|
const old = {
|
||||||
|
@ -51,9 +51,6 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("normalizeNeighbors", () => {
|
describe("normalizeNeighbors", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
|
||||||
const n3 = NodeAddress.fromParts(["n3"]);
|
|
||||||
// This chain isn't a proper stochastic chain, but that's okay:
|
// This chain isn't a proper stochastic chain, but that's okay:
|
||||||
// the actual values aren't relevant.
|
// the actual values aren't relevant.
|
||||||
const old = {
|
const old = {
|
||||||
|
@ -86,13 +83,11 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
// The tests for `createOrderedSparseMarkovChain` also must invoke
|
// The tests for `createOrderedSparseMarkovChain` also must invoke
|
||||||
// `createConnections`, so we add only light testing separately.
|
// `createConnections`, so we add only light testing separately.
|
||||||
it("works on a simple asymmetric chain", () => {
|
it("works on a simple asymmetric chain", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const e1 = edge("e1", n1, n2);
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
const e2 = edge("e2", n2, n3);
|
||||||
const n3 = NodeAddress.fromParts(["sink"]);
|
const e3 = edge("e3", n1, n3);
|
||||||
const e1 = {src: n1, dst: n2, address: EdgeAddress.fromParts(["e1"])};
|
const e4 = edge("e4", n3, n3);
|
||||||
const e2 = {src: n2, dst: n3, address: EdgeAddress.fromParts(["e2"])};
|
|
||||||
const e3 = {src: n1, dst: n3, address: EdgeAddress.fromParts(["e3"])};
|
|
||||||
const e4 = {src: n3, dst: n3, address: EdgeAddress.fromParts(["e4"])};
|
|
||||||
const g = new Graph()
|
const g = new Graph()
|
||||||
.addNode(n1)
|
.addNode(n1)
|
||||||
.addNode(n2)
|
.addNode(n2)
|
||||||
|
@ -133,8 +128,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
|
|
||||||
describe("createOrderedSparseMarkovChain", () => {
|
describe("createOrderedSparseMarkovChain", () => {
|
||||||
it("works on a trivial one-node chain with no edge", () => {
|
it("works on a trivial one-node chain with no edge", () => {
|
||||||
const n = NodeAddress.fromParts(["foo"]);
|
const g = new Graph().addNode(n1);
|
||||||
const g = new Graph().addNode(n);
|
|
||||||
const edgeWeight = (_unused_edge) => {
|
const edgeWeight = (_unused_edge) => {
|
||||||
throw new Error("Don't even look at me");
|
throw new Error("Don't even look at me");
|
||||||
};
|
};
|
||||||
|
@ -142,7 +136,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
createConnections(g, edgeWeight, 1e-3)
|
createConnections(g, edgeWeight, 1e-3)
|
||||||
);
|
);
|
||||||
const expected = {
|
const expected = {
|
||||||
nodeOrder: [n],
|
nodeOrder: [n1],
|
||||||
chain: [
|
chain: [
|
||||||
{neighbor: new Uint32Array([0]), weight: new Float64Array([1.0])},
|
{neighbor: new Uint32Array([0]), weight: new Float64Array([1.0])},
|
||||||
],
|
],
|
||||||
|
@ -151,13 +145,11 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("works on a simple asymmetric chain", () => {
|
it("works on a simple asymmetric chain", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const e1 = edge("e1", n1, n2);
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
const e2 = edge("e2", n2, n3);
|
||||||
const n3 = NodeAddress.fromParts(["sink"]);
|
const e3 = edge("e3", n1, n3);
|
||||||
const e1 = {src: n1, dst: n2, address: EdgeAddress.fromParts(["e1"])};
|
const e4 = edge("e4", n3, n3);
|
||||||
const e2 = {src: n2, dst: n3, address: EdgeAddress.fromParts(["e2"])};
|
|
||||||
const e3 = {src: n1, dst: n3, address: EdgeAddress.fromParts(["e3"])};
|
|
||||||
const e4 = {src: n3, dst: n3, address: EdgeAddress.fromParts(["e4"])};
|
|
||||||
const g = new Graph()
|
const g = new Graph()
|
||||||
.addNode(n1)
|
.addNode(n1)
|
||||||
.addNode(n2)
|
.addNode(n2)
|
||||||
|
@ -191,12 +183,9 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("works on a symmetric K_3", () => {
|
it("works on a symmetric K_3", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const e1 = edge("e1", n1, n2);
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
const e2 = edge("e2", n2, n3);
|
||||||
const n3 = NodeAddress.fromParts(["n3"]);
|
const e3 = edge("e3", n3, n1);
|
||||||
const e1 = {src: n1, dst: n2, address: EdgeAddress.fromParts(["e1"])};
|
|
||||||
const e2 = {src: n2, dst: n3, address: EdgeAddress.fromParts(["e2"])};
|
|
||||||
const e3 = {src: n3, dst: n1, address: EdgeAddress.fromParts(["e3"])};
|
|
||||||
const g = new Graph()
|
const g = new Graph()
|
||||||
.addNode(n1)
|
.addNode(n1)
|
||||||
.addNode(n2)
|
.addNode(n2)
|
||||||
|
@ -257,10 +246,10 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
// - dst: `epsilon / 2` from dst, `(8 - epsilon) / 8` from src
|
// - dst: `epsilon / 2` from dst, `(8 - epsilon) / 8` from src
|
||||||
const expected = {
|
const expected = {
|
||||||
nodeOrder: [
|
nodeOrder: [
|
||||||
ag.nodes.src(),
|
ag.nodes.src,
|
||||||
ag.nodes.dst(),
|
ag.nodes.dst,
|
||||||
ag.nodes.loop(),
|
ag.nodes.loop,
|
||||||
ag.nodes.isolated(),
|
ag.nodes.isolated,
|
||||||
],
|
],
|
||||||
chain: [
|
chain: [
|
||||||
{
|
{
|
||||||
|
@ -282,8 +271,6 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
describe("distributionToNodeDistribution", () => {
|
describe("distributionToNodeDistribution", () => {
|
||||||
it("works", () => {
|
it("works", () => {
|
||||||
const pi = new Float64Array([0.25, 0.75]);
|
const pi = new Float64Array([0.25, 0.75]);
|
||||||
const n1 = NodeAddress.fromParts(["foo"]);
|
|
||||||
const n2 = NodeAddress.fromParts(["bar"]);
|
|
||||||
expect(distributionToNodeDistribution([n1, n2], pi)).toEqual(
|
expect(distributionToNodeDistribution([n1, n2], pi)).toEqual(
|
||||||
new Map().set(n1, 0.25).set(n2, 0.75)
|
new Map().set(n1, 0.25).set(n2, 0.75)
|
||||||
);
|
);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,60 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import {EdgeAddress, Graph, NodeAddress} from "./graph";
|
import {
|
||||||
|
EdgeAddress,
|
||||||
|
Graph,
|
||||||
|
NodeAddress,
|
||||||
|
type NodeAddressT,
|
||||||
|
type Edge,
|
||||||
|
} from "./graph";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new NodeAddressT from an array of string address parts.
|
||||||
|
*
|
||||||
|
* Note: This is included as a preliminary clean-up method so that it will be easy to
|
||||||
|
* switch Graph nodes from being represented by a NodeAddressT to a rich Node object.
|
||||||
|
* In a followon commit, this method will create a Node instead of a NodeAddressT.
|
||||||
|
*/
|
||||||
|
export function partsNode(parts: string[]): NodeAddressT {
|
||||||
|
return NodeAddress.fromParts(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Node from a single address part.
|
||||||
|
*
|
||||||
|
* The same considerations as partsNode apply.
|
||||||
|
*/
|
||||||
|
export function node(name: string): NodeAddressT {
|
||||||
|
return partsNode([name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Edge from address parts and a src and dst.
|
||||||
|
*
|
||||||
|
* This is a convenience method for constructing example edges more concisely in test code.
|
||||||
|
*
|
||||||
|
* The returned edge is frozen, so it is safe to use across test cases.
|
||||||
|
*/
|
||||||
|
export function partsEdge(
|
||||||
|
parts: string[],
|
||||||
|
src: NodeAddressT,
|
||||||
|
dst: NodeAddressT
|
||||||
|
): Edge {
|
||||||
|
return Object.freeze({
|
||||||
|
address: EdgeAddress.fromParts(parts),
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Edge from a single address part and a src and dst.
|
||||||
|
*
|
||||||
|
* The same considerations as partsEdge apply.
|
||||||
|
*/
|
||||||
|
export function edge(name: string, src: NodeAddressT, dst: NodeAddressT): Edge {
|
||||||
|
return partsEdge([name], src, dst);
|
||||||
|
}
|
||||||
|
|
||||||
export function advancedGraph() {
|
export function advancedGraph() {
|
||||||
// The advanced graph has the following features:
|
// The advanced graph has the following features:
|
||||||
|
@ -13,88 +67,69 @@ export function advancedGraph() {
|
||||||
// logically equivalent but very different history
|
// logically equivalent but very different history
|
||||||
// To avoid contamination, every piece is exposed as a function
|
// To avoid contamination, every piece is exposed as a function
|
||||||
// which generates a clean copy of that piece.
|
// which generates a clean copy of that piece.
|
||||||
const src = () => NodeAddress.fromParts(["src"]);
|
const src = node("src");
|
||||||
const dst = () => NodeAddress.fromParts(["dst"]);
|
const dst = node("dst");
|
||||||
const hom1 = () => ({
|
const hom1 = partsEdge(["hom", "1"], src, dst);
|
||||||
src: src(),
|
const hom2 = partsEdge(["hom", "2"], src, dst);
|
||||||
dst: dst(),
|
const loop = node("loop");
|
||||||
address: EdgeAddress.fromParts(["hom", "1"]),
|
const loop_loop = edge("loop", loop, loop);
|
||||||
});
|
const isolated = node("isolated");
|
||||||
const hom2 = () => ({
|
|
||||||
src: src(),
|
|
||||||
dst: dst(),
|
|
||||||
address: EdgeAddress.fromParts(["hom", "2"]),
|
|
||||||
});
|
|
||||||
const loop = () => NodeAddress.fromParts(["loop"]);
|
|
||||||
const loop_loop = () => ({
|
|
||||||
src: loop(),
|
|
||||||
dst: loop(),
|
|
||||||
address: EdgeAddress.fromParts(["loop"]),
|
|
||||||
});
|
|
||||||
const isolated = () => NodeAddress.fromParts(["isolated"]);
|
|
||||||
const graph1 = () =>
|
const graph1 = () =>
|
||||||
new Graph()
|
new Graph()
|
||||||
.addNode(src())
|
.addNode(src)
|
||||||
.addNode(dst())
|
.addNode(dst)
|
||||||
.addNode(loop())
|
.addNode(loop)
|
||||||
.addNode(isolated())
|
.addNode(isolated)
|
||||||
.addEdge(hom1())
|
.addEdge(hom1)
|
||||||
.addEdge(hom2())
|
.addEdge(hom2)
|
||||||
.addEdge(loop_loop());
|
.addEdge(loop_loop);
|
||||||
|
|
||||||
// graph2 is logically equivalent to graph1, but is constructed with very
|
// graph2 is logically equivalent to graph1, but is constructed with very
|
||||||
// different history.
|
// different history.
|
||||||
// Use this to check that logically equivalent graphs are treated
|
// Use this to check that logically equivalent graphs are treated
|
||||||
// equivalently, regardless of their history.
|
// equivalently, regardless of their history.
|
||||||
const phantomNode = () => NodeAddress.fromParts(["phantom"]);
|
const phantomNode = node("phantom");
|
||||||
const phantomEdge1 = () => ({
|
const phantomEdge1 = edge("phantom", src, phantomNode);
|
||||||
src: src(),
|
const phantomEdge2 = edge("not-so-isolated", src, isolated);
|
||||||
dst: phantomNode(),
|
|
||||||
address: EdgeAddress.fromParts(["phantom"]),
|
|
||||||
});
|
|
||||||
const phantomEdge2 = () => ({
|
|
||||||
src: src(),
|
|
||||||
dst: isolated(),
|
|
||||||
address: EdgeAddress.fromParts(["not", "so", "isolated"]),
|
|
||||||
});
|
|
||||||
// To verify that the graphs are equivalent, every mutation is preceded
|
// To verify that the graphs are equivalent, every mutation is preceded
|
||||||
// by a comment stating what the set of nodes and edges are prior to that mutation
|
// by a comment stating what the set of nodes and edges are prior to that mutation
|
||||||
const graph2 = () =>
|
const graph2 = () =>
|
||||||
new Graph()
|
new Graph()
|
||||||
// N: [], E: []
|
// N: [], E: []
|
||||||
.addNode(phantomNode())
|
.addNode(phantomNode)
|
||||||
// N: [phantomNode], E: []
|
// N: [phantomNode], E: []
|
||||||
.addNode(src())
|
.addNode(src)
|
||||||
// N: [phantomNode, src], E: []
|
// N: [phantomNode, src], E: []
|
||||||
.addEdge(phantomEdge1())
|
.addEdge(phantomEdge1)
|
||||||
// N: [phantomNode, src], E: [phantomEdge1]
|
// N: [phantomNode, src], E: [phantomEdge1]
|
||||||
.addNode(isolated())
|
.addNode(isolated)
|
||||||
// N: [phantomNode, src, isolated], E: [phantomEdge1]
|
// N: [phantomNode, src, isolated], E: [phantomEdge1]
|
||||||
.removeEdge(phantomEdge1().address)
|
.removeEdge(phantomEdge1.address)
|
||||||
// N: [phantomNode, src, isolated], E: []
|
// N: [phantomNode, src, isolated], E: []
|
||||||
.addNode(dst())
|
.addNode(dst)
|
||||||
// N: [phantomNode, src, isolated, dst], E: []
|
// N: [phantomNode, src, isolated, dst], E: []
|
||||||
.addEdge(hom1())
|
.addEdge(hom1)
|
||||||
// N: [phantomNode, src, isolated, dst], E: [hom1]
|
// N: [phantomNode, src, isolated, dst], E: [hom1]
|
||||||
.addEdge(phantomEdge2())
|
.addEdge(phantomEdge2)
|
||||||
// N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2]
|
// N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2]
|
||||||
.addEdge(hom2())
|
.addEdge(hom2)
|
||||||
// N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2, hom2]
|
// N: [phantomNode, src, isolated, dst], E: [hom1, phantomEdge2, hom2]
|
||||||
.removeEdge(hom1().address)
|
.removeEdge(hom1.address)
|
||||||
// N: [phantomNode, src, isolated, dst], E: [phantomEdge2, hom2]
|
// N: [phantomNode, src, isolated, dst], E: [phantomEdge2, hom2]
|
||||||
.removeNode(phantomNode())
|
.removeNode(phantomNode)
|
||||||
// N: [src, isolated, dst], E: [phantomEdge2, hom2]
|
// N: [src, isolated, dst], E: [phantomEdge2, hom2]
|
||||||
.removeEdge(phantomEdge2().address)
|
.removeEdge(phantomEdge2.address)
|
||||||
// N: [src, isolated, dst], E: [hom2]
|
// N: [src, isolated, dst], E: [hom2]
|
||||||
.removeNode(isolated())
|
.removeNode(isolated)
|
||||||
// N: [src, dst], E: [hom2]
|
// N: [src, dst], E: [hom2]
|
||||||
.addNode(isolated())
|
.addNode(isolated)
|
||||||
// N: [src, dst, isolated], E: [hom2]
|
// N: [src, dst, isolated], E: [hom2]
|
||||||
.addNode(loop())
|
.addNode(loop)
|
||||||
// N: [src, dst, isolated, loop], E: [hom2]
|
// N: [src, dst, isolated, loop], E: [hom2]
|
||||||
.addEdge(loop_loop())
|
.addEdge(loop_loop)
|
||||||
// N: [src, dst, isolated, loop], E: [hom2, loop_loop]
|
// N: [src, dst, isolated, loop], E: [hom2, loop_loop]
|
||||||
.addEdge(hom1());
|
.addEdge(hom1);
|
||||||
// N: [src, dst, isolated, loop], E: [hom2, loop_loop, hom1]
|
// N: [src, dst, isolated, loop], E: [hom2, loop_loop, hom1]
|
||||||
const nodes = {src, dst, loop, isolated, phantomNode};
|
const nodes = {src, dst, loop, isolated, phantomNode};
|
||||||
const edges = {hom1, hom2, loop_loop, phantomEdge1, phantomEdge2};
|
const edges = {hom1, hom2, loop_loop, phantomEdge1, phantomEdge2};
|
||||||
|
|
|
@ -17,13 +17,12 @@ import {
|
||||||
DEFAULT_ALPHA,
|
DEFAULT_ALPHA,
|
||||||
DEFAULT_SEED,
|
DEFAULT_SEED,
|
||||||
} from "./pagerankGraph";
|
} from "./pagerankGraph";
|
||||||
import {advancedGraph} from "./graphTestUtil";
|
import {advancedGraph, node, partsNode, partsEdge} from "./graphTestUtil";
|
||||||
import * as NullUtil from "../util/null";
|
import * as NullUtil from "../util/null";
|
||||||
|
|
||||||
describe("core/pagerankGraph", () => {
|
describe("core/pagerankGraph", () => {
|
||||||
const defaultEvaluator = (_unused_edge) => ({toWeight: 1, froWeight: 0});
|
const defaultEvaluator = (_unused_edge) => ({toWeight: 1, froWeight: 0});
|
||||||
const nonEmptyGraph = () =>
|
const nonEmptyGraph = () => new Graph().addNode(node("hi"));
|
||||||
new Graph().addNode(NodeAddress.fromParts(["hi"]));
|
|
||||||
|
|
||||||
function examplePagerankGraph(
|
function examplePagerankGraph(
|
||||||
edgeEvaluator = defaultEvaluator
|
edgeEvaluator = defaultEvaluator
|
||||||
|
@ -39,9 +38,7 @@ describe("core/pagerankGraph", () => {
|
||||||
|
|
||||||
it("cannot construct PagerankGraph with empty Graph", () => {
|
it("cannot construct PagerankGraph with empty Graph", () => {
|
||||||
const eg1 = new Graph();
|
const eg1 = new Graph();
|
||||||
const eg2 = new Graph()
|
const eg2 = new Graph().addNode(node("hi")).removeNode(node("hi"));
|
||||||
.addNode(NodeAddress.empty)
|
|
||||||
.removeNode(NodeAddress.empty);
|
|
||||||
expect(() => new PagerankGraph(eg1, defaultEvaluator)).toThrowError(
|
expect(() => new PagerankGraph(eg1, defaultEvaluator)).toThrowError(
|
||||||
"empty graph"
|
"empty graph"
|
||||||
);
|
);
|
||||||
|
@ -93,8 +90,7 @@ describe("core/pagerankGraph", () => {
|
||||||
const pg = new PagerankGraph(g, defaultEvaluator);
|
const pg = new PagerankGraph(g, defaultEvaluator);
|
||||||
const graphNodes = Array.from(g.nodes());
|
const graphNodes = Array.from(g.nodes());
|
||||||
const pgNodes = Array.from(pg.nodes()).map((x) => x.address);
|
const pgNodes = Array.from(pg.nodes()).map((x) => x.address);
|
||||||
expect(graphNodes.length).toEqual(pgNodes.length);
|
expect(graphNodes).toEqual(pgNodes);
|
||||||
expect(new Set(graphNodes)).toEqual(new Set(pgNodes));
|
|
||||||
});
|
});
|
||||||
it("node and nodes both return consistent scores", async () => {
|
it("node and nodes both return consistent scores", async () => {
|
||||||
const pg = await convergedPagerankGraph();
|
const pg = await convergedPagerankGraph();
|
||||||
|
@ -104,7 +100,7 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
it("node and nodes both throw an error if underlying graph is modified", () => {
|
it("node and nodes both throw an error if underlying graph is modified", () => {
|
||||||
const pg = new PagerankGraph(nonEmptyGraph(), defaultEvaluator);
|
const pg = new PagerankGraph(nonEmptyGraph(), defaultEvaluator);
|
||||||
pg.graph().addNode(NodeAddress.empty);
|
pg.graph().addNode(node("foo"));
|
||||||
expect(() => pg.nodes()).toThrowError(
|
expect(() => pg.nodes()).toThrowError(
|
||||||
"underlying Graph has been modified"
|
"underlying Graph has been modified"
|
||||||
);
|
);
|
||||||
|
@ -115,10 +111,10 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("node prefix filter matches graph filter", () => {
|
describe("node prefix filter matches graph filter", () => {
|
||||||
const n1 = NodeAddress.empty;
|
const n1 = partsNode([]);
|
||||||
const n2 = NodeAddress.fromParts(["foo"]);
|
const n2 = partsNode(["foo"]);
|
||||||
const n3 = NodeAddress.fromParts(["foo", "bar"]);
|
const n3 = partsNode(["foo", "bar"]);
|
||||||
const n4 = NodeAddress.fromParts(["zod", "bar"]);
|
const n4 = partsNode(["zod", "bar"]);
|
||||||
const g = () =>
|
const g = () =>
|
||||||
new Graph()
|
new Graph()
|
||||||
.addNode(n1)
|
.addNode(n1)
|
||||||
|
@ -200,7 +196,7 @@ describe("core/pagerankGraph", () => {
|
||||||
|
|
||||||
it("edge and edges both throw an error if underlying graph is modified", () => {
|
it("edge and edges both throw an error if underlying graph is modified", () => {
|
||||||
const pg = new PagerankGraph(nonEmptyGraph(), defaultEvaluator);
|
const pg = new PagerankGraph(nonEmptyGraph(), defaultEvaluator);
|
||||||
pg.graph().addNode(NodeAddress.empty);
|
pg.graph().addNode(node("foo"));
|
||||||
expect(() => pg.edges()).toThrowError(
|
expect(() => pg.edges()).toThrowError(
|
||||||
"underlying Graph has been modified"
|
"underlying Graph has been modified"
|
||||||
);
|
);
|
||||||
|
@ -213,10 +209,10 @@ describe("core/pagerankGraph", () => {
|
||||||
describe("totalOutWeight", () => {
|
describe("totalOutWeight", () => {
|
||||||
it("errors on a modified graph", () => {
|
it("errors on a modified graph", () => {
|
||||||
const eg = examplePagerankGraph();
|
const eg = examplePagerankGraph();
|
||||||
eg.graph().addNode(NodeAddress.fromParts(["bad", "node"]));
|
eg.graph().addNode(partsNode(["bad", "node"]));
|
||||||
expect(() =>
|
expect(() => eg.totalOutWeight(partsNode(["bad", "node"]))).toThrowError(
|
||||||
eg.totalOutWeight(NodeAddress.fromParts(["bad", "node"]))
|
"has been modified"
|
||||||
).toThrowError("has been modified");
|
);
|
||||||
});
|
});
|
||||||
it("errors on nonexistent node", () => {
|
it("errors on nonexistent node", () => {
|
||||||
const eg = examplePagerankGraph();
|
const eg = examplePagerankGraph();
|
||||||
|
@ -271,30 +267,14 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge filtering", () => {
|
describe("edge filtering", () => {
|
||||||
const src1 = NodeAddress.fromParts(["src", "1"]);
|
const src1 = partsNode(["src", "1"]);
|
||||||
const src2 = NodeAddress.fromParts(["src", "2"]);
|
const src2 = partsNode(["src", "2"]);
|
||||||
const dst1 = NodeAddress.fromParts(["dst", "1"]);
|
const dst1 = partsNode(["dst", "1"]);
|
||||||
const dst2 = NodeAddress.fromParts(["dst", "2"]);
|
const dst2 = partsNode(["dst", "2"]);
|
||||||
const e11 = {
|
const e11 = partsEdge(["e", "1", "1"], src1, dst1);
|
||||||
src: src1,
|
const e12 = partsEdge(["e", "1", "2"], src1, dst2);
|
||||||
dst: dst1,
|
const e21 = partsEdge(["e", "2", "1"], src2, dst1);
|
||||||
address: EdgeAddress.fromParts(["e", "1", "1"]),
|
const e22 = partsEdge(["e", "2", "2"], src2, dst2);
|
||||||
};
|
|
||||||
const e12 = {
|
|
||||||
src: src1,
|
|
||||||
dst: dst2,
|
|
||||||
address: EdgeAddress.fromParts(["e", "1", "2"]),
|
|
||||||
};
|
|
||||||
const e21 = {
|
|
||||||
src: src2,
|
|
||||||
dst: dst1,
|
|
||||||
address: EdgeAddress.fromParts(["e", "2", "1"]),
|
|
||||||
};
|
|
||||||
const e22 = {
|
|
||||||
src: src2,
|
|
||||||
dst: dst2,
|
|
||||||
address: EdgeAddress.fromParts(["e", "2", "2"]),
|
|
||||||
};
|
|
||||||
const graph = () => {
|
const graph = () => {
|
||||||
const g = new Graph();
|
const g = new Graph();
|
||||||
[src1, src2, dst1, dst2].forEach((n) => g.addNode(n));
|
[src1, src2, dst1, dst2].forEach((n) => g.addNode(n));
|
||||||
|
@ -395,7 +375,7 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
it("is an error to call neighbors after modifying the underlying graph", () => {
|
it("is an error to call neighbors after modifying the underlying graph", () => {
|
||||||
const pg = examplePagerankGraph();
|
const pg = examplePagerankGraph();
|
||||||
pg.graph().addNode(NodeAddress.fromParts(["foomfazzle"]));
|
pg.graph().addNode(partsNode(["foomfazzle"]));
|
||||||
expect(() =>
|
expect(() =>
|
||||||
pg.neighbors(NodeAddress.fromParts(["src"]), allNeighbors())
|
pg.neighbors(NodeAddress.fromParts(["src"]), allNeighbors())
|
||||||
).toThrowError("has been modified");
|
).toThrowError("has been modified");
|
||||||
|
@ -549,8 +529,8 @@ describe("core/pagerankGraph", () => {
|
||||||
const pg1 = examplePagerankGraph();
|
const pg1 = examplePagerankGraph();
|
||||||
const pg2 = examplePagerankGraph();
|
const pg2 = examplePagerankGraph();
|
||||||
const {nodes} = advancedGraph();
|
const {nodes} = advancedGraph();
|
||||||
const seed1 = new Map().set(nodes.src(), 1);
|
const seed1 = new Map().set(nodes.src, 1);
|
||||||
const seed2 = new Map().set(nodes.dst(), 1);
|
const seed2 = new Map().set(nodes.dst, 1);
|
||||||
await pg1.runPagerank({seed: seed1, alpha: 0});
|
await pg1.runPagerank({seed: seed1, alpha: 0});
|
||||||
await pg2.runPagerank({seed: seed2, alpha: 0});
|
await pg2.runPagerank({seed: seed2, alpha: 0});
|
||||||
expect(pg1.equals(pg2)).toBe(true);
|
expect(pg1.equals(pg2)).toBe(true);
|
||||||
|
@ -559,16 +539,16 @@ describe("core/pagerankGraph", () => {
|
||||||
it("seed is returned directly if alpha is 1", async () => {
|
it("seed is returned directly if alpha is 1", async () => {
|
||||||
const pg = examplePagerankGraph();
|
const pg = examplePagerankGraph();
|
||||||
const src = advancedGraph().nodes.src;
|
const src = advancedGraph().nodes.src;
|
||||||
const seed = new Map().set(src(), 1);
|
const seed = new Map().set(src, 1);
|
||||||
await pg.runPagerank({seed, alpha: 1});
|
await pg.runPagerank({seed, alpha: 1});
|
||||||
const score = NullUtil.get(pg.node(src())).score;
|
const score = NullUtil.get(pg.node(src)).score;
|
||||||
expect(score).toBe(1);
|
expect(score).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("promise rejects if the graph was modified", async () => {
|
it("promise rejects if the graph was modified", async () => {
|
||||||
const pg = examplePagerankGraph();
|
const pg = examplePagerankGraph();
|
||||||
pg.graph().addNode(NodeAddress.empty);
|
pg.graph().addNode(node("foo"));
|
||||||
expect(
|
expect(
|
||||||
pg.runPagerank({maxIterations: 1, convergenceThreshold: 1})
|
pg.runPagerank({maxIterations: 1, convergenceThreshold: 1})
|
||||||
).rejects.toThrow("underlying Graph has been modified");
|
).rejects.toThrow("underlying Graph has been modified");
|
||||||
|
@ -644,7 +624,7 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
it("unequal graph => unequal", () => {
|
it("unequal graph => unequal", () => {
|
||||||
const pg1 = new PagerankGraph(nonEmptyGraph(), defaultEvaluator, 0.1);
|
const pg1 = new PagerankGraph(nonEmptyGraph(), defaultEvaluator, 0.1);
|
||||||
const g2 = nonEmptyGraph().addNode(NodeAddress.empty);
|
const g2 = nonEmptyGraph().addNode(node("foo"));
|
||||||
const pg2 = new PagerankGraph(g2, defaultEvaluator, 0.1);
|
const pg2 = new PagerankGraph(g2, defaultEvaluator, 0.1);
|
||||||
expect(pg1.equals(pg2)).toBe(false);
|
expect(pg1.equals(pg2)).toBe(false);
|
||||||
});
|
});
|
||||||
|
@ -676,7 +656,7 @@ describe("core/pagerankGraph", () => {
|
||||||
});
|
});
|
||||||
it("throws an error if the underlying graph is modified", () => {
|
it("throws an error if the underlying graph is modified", () => {
|
||||||
const pg = examplePagerankGraph();
|
const pg = examplePagerankGraph();
|
||||||
pg.graph().addNode(NodeAddress.fromParts(["modification"]));
|
pg.graph().addNode(node("modification"));
|
||||||
expect(() => pg.equals(pg)).toThrowError("has been modified");
|
expect(() => pg.equals(pg)).toThrowError("has been modified");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import {Graph, NodeAddress, EdgeAddress} from "../../core/graph";
|
import {Graph} from "../../core/graph";
|
||||||
|
import {partsNode, partsEdge} from "../../core/graphTestUtil";
|
||||||
|
|
||||||
export const nodes = Object.freeze({
|
export const nodes = Object.freeze({
|
||||||
inserter1: NodeAddress.fromParts(["factorio", "inserter", "1"]),
|
inserter1: partsNode(["factorio", "inserter", "1"]),
|
||||||
machine1: NodeAddress.fromParts(["factorio", "machine", "1"]),
|
machine1: partsNode(["factorio", "machine", "1"]),
|
||||||
inserter2: NodeAddress.fromParts(["factorio", "inserter", "2"]),
|
inserter2: partsNode(["factorio", "inserter", "2"]),
|
||||||
machine2: NodeAddress.fromParts(["factorio", "machine", "2"]),
|
machine2: partsNode(["factorio", "machine", "2"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const edges = Object.freeze({
|
export const edges = Object.freeze({
|
||||||
transports1: Object.freeze({
|
transports1: partsEdge(
|
||||||
src: nodes.inserter1,
|
["factorio", "transports", "1"],
|
||||||
dst: nodes.machine1,
|
nodes.inserter1,
|
||||||
address: EdgeAddress.fromParts(["factorio", "transports", "1"]),
|
nodes.machine1
|
||||||
}),
|
),
|
||||||
assembles1: Object.freeze({
|
transports2: partsEdge(
|
||||||
src: nodes.machine1,
|
["factorio", "transports", "2"],
|
||||||
dst: nodes.inserter2,
|
nodes.inserter2,
|
||||||
address: EdgeAddress.fromParts(["factorio", "assembles", "1"]),
|
nodes.machine2
|
||||||
}),
|
),
|
||||||
transports2: Object.freeze({
|
assembles1: partsEdge(
|
||||||
src: nodes.inserter2,
|
["factorio", "assembles", "1"],
|
||||||
dst: nodes.machine2,
|
nodes.machine1,
|
||||||
address: EdgeAddress.fromParts(["factorio", "assembles", "2"]),
|
nodes.inserter2
|
||||||
}),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function graph() {
|
export function graph() {
|
||||||
|
|
Loading…
Reference in New Issue