Remove Graph.{in,out}Edges (#174)
This method removes `Graph.inEdges` and `Graph.outEdges`. As a replacement to these two functions, `graph.neighborhood` now takes an optional `direction` flag, which can be set to `"IN" | "OUT" | "ANY"`. This reduces the surface area of the Graph API, and means that the same pattern can be used when requesting in or out neighbors as is used when requesting all neighbors. This change generates significant churn in the test files, and in some cases the tests are less elegant / show historicity, as they were written for the type signature of `{in,out}Edges`, which just returns an array of edges, and now receive an array of neighbors. I think this is acceptable, and it's not worth re-writing the test. In many cases, replacing existing calls to `{in,out}Edges` in our actual codebase resulted in cleaner code, as `neighborhood` successfully abstracts over the common patterns that users of `{in,out}Edges` were implementing. As a fly-by refactor, I also changed the `neighborAddress` part of the `neighborhood` return value to `neighbor`. It's a little less descriptive, but it's more concise, and flow is there to help ensure it's used correctly. Test plan: Note that CI passes. Inspect the test changes, and verify that they are appropriate transformations for consuming the new API.
This commit is contained in:
parent
5af5748ed7
commit
0609201af4
|
@ -169,78 +169,70 @@ export class Graph<NP, EP> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the array of all out-edges from the node at the given address.
|
* Find the neighborhood of the node at the given address.
|
||||||
* The order of the resulting array is unspecified.
|
|
||||||
*/
|
|
||||||
outEdges(
|
|
||||||
nodeAddress: Address,
|
|
||||||
typeOptions?: {+nodeType?: string, +edgeType?: string}
|
|
||||||
): Edge<EP>[] {
|
|
||||||
if (nodeAddress == null) {
|
|
||||||
throw new Error(`address is ${String(nodeAddress)}`);
|
|
||||||
}
|
|
||||||
let result = this._lookupEdges(this._outEdges, nodeAddress).map((e) =>
|
|
||||||
this.edge(e)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeOptions != null && typeOptions.edgeType != null) {
|
|
||||||
const edgeType = typeOptions.edgeType;
|
|
||||||
result = result.filter((e) => e.address.type === edgeType);
|
|
||||||
}
|
|
||||||
if (typeOptions != null && typeOptions.nodeType != null) {
|
|
||||||
const nodeType = typeOptions.nodeType;
|
|
||||||
result = result.filter((e) => e.dst.type === nodeType);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the array of all in-edges to the node at the given address.
|
|
||||||
* The order of the resulting array is unspecified.
|
|
||||||
*/
|
|
||||||
inEdges(
|
|
||||||
nodeAddress: Address,
|
|
||||||
typeOptions?: {+nodeType?: string, +edgeType?: string}
|
|
||||||
): Edge<EP>[] {
|
|
||||||
if (nodeAddress == null) {
|
|
||||||
throw new Error(`address is ${String(nodeAddress)}`);
|
|
||||||
}
|
|
||||||
let result = this._lookupEdges(this._inEdges, nodeAddress).map((e) =>
|
|
||||||
this.edge(e)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeOptions != null && typeOptions.edgeType != null) {
|
|
||||||
const edgeType = typeOptions.edgeType;
|
|
||||||
result = result.filter((e) => e.address.type === edgeType);
|
|
||||||
}
|
|
||||||
if (typeOptions != null && typeOptions.nodeType != null) {
|
|
||||||
const nodeType = typeOptions.nodeType;
|
|
||||||
result = result.filter((e) => e.src.type === nodeType);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the neighborhood of a given nodeAddress
|
|
||||||
*
|
*
|
||||||
* By neighborhood, we mean every `{edge, neighborAddress}` such that edge
|
* By neighborhood, we mean every `{edge, neighbor}` such that edge
|
||||||
* has `nodeAddress` as either `src` or `dst`, and `neighborAddress` is the
|
* has `nodeAddress` as either `src` or `dst`, and `neighbor` is the
|
||||||
* address at the other end of the edge.
|
* address at the other end of the edge.
|
||||||
|
*
|
||||||
|
* The returned neighbors are filtered according to the `options`. Callers
|
||||||
|
* can filter by nodeType, edgeType, and whether it should be an "IN" edge
|
||||||
|
* (i.e. the provided node is the dst), an "OUT" edge (i.e. provided node is
|
||||||
|
* the src), or "ANY".
|
||||||
*/
|
*/
|
||||||
neighborhood(
|
neighborhood(
|
||||||
nodeAddress: Address,
|
nodeAddress: Address,
|
||||||
typeOptions?: {+nodeType?: string, +edgeType?: string}
|
options?: {|
|
||||||
): {+edge: Edge<EP>, +neighborAddress: Address}[] {
|
+nodeType?: string,
|
||||||
const inNeighbors = this.inEdges(nodeAddress, typeOptions).map((e) => {
|
+edgeType?: string,
|
||||||
return {edge: e, neighborAddress: e.src};
|
+direction?: "IN" | "OUT" | "ANY",
|
||||||
});
|
|}
|
||||||
const outNeighbors = this.outEdges(nodeAddress, typeOptions)
|
): {+edge: Edge<EP>, +neighbor: Address}[] {
|
||||||
// If there are self-reference edges, avoid double counting them.
|
if (nodeAddress == null) {
|
||||||
.filter((e) => !deepEqual(e.src, e.dst))
|
throw new Error(`address is ${String(nodeAddress)}`);
|
||||||
.map((e) => {
|
}
|
||||||
return {edge: e, neighborAddress: e.dst};
|
|
||||||
});
|
let result: {+edge: Edge<EP>, +neighbor: Address}[] = [];
|
||||||
return [].concat(inNeighbors, outNeighbors);
|
const direction = (options != null && options.direction) || "ANY";
|
||||||
|
|
||||||
|
if (direction === "ANY" || direction === "IN") {
|
||||||
|
let inNeighbors = this._lookupEdges(this._inEdges, nodeAddress).map(
|
||||||
|
(e) => {
|
||||||
|
const edge = this.edge(e);
|
||||||
|
return {edge, neighbor: edge.src};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
result = result.concat(inNeighbors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === "ANY" || direction === "OUT") {
|
||||||
|
let outNeighbors = this._lookupEdges(this._outEdges, nodeAddress).map(
|
||||||
|
(e) => {
|
||||||
|
const edge = this.edge(e);
|
||||||
|
return {edge, neighbor: edge.dst};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (direction === "ANY") {
|
||||||
|
// If direction is ANY, we already counted self-referencing edges as
|
||||||
|
// an inNeighbor
|
||||||
|
outNeighbors = outNeighbors.filter(
|
||||||
|
({edge}) => !deepEqual(edge.src, edge.dst)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.concat(outNeighbors);
|
||||||
|
}
|
||||||
|
if (options != null && options.edgeType != null) {
|
||||||
|
const edgeType = options.edgeType;
|
||||||
|
result = result.filter(({edge}) => edge.address.type === edgeType);
|
||||||
|
}
|
||||||
|
if (options != null && options.nodeType != null) {
|
||||||
|
const nodeType = options.nodeType;
|
||||||
|
result = result.filter(({neighbor}) => neighbor.type === nodeType);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -65,13 +65,8 @@ describe("graph", () => {
|
||||||
`address is ${String(bad)}`
|
`address is ${String(bad)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it(`getting ${String(bad)} in-edges`, () => {
|
it(`getting ${String(bad)} neighborhood`, () => {
|
||||||
expect(() => new Graph().inEdges((bad: any))).toThrow(
|
expect(() => new Graph().neighborhood((bad: any))).toThrow(
|
||||||
`address is ${String(bad)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it(`getting ${String(bad)} out-edges`, () => {
|
|
||||||
expect(() => new Graph().outEdges((bad: any))).toThrow(
|
|
||||||
`address is ${String(bad)}`
|
`address is ${String(bad)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -298,12 +293,16 @@ describe("graph", () => {
|
||||||
it("allows creating self-loops", () => {
|
it("allows creating self-loops", () => {
|
||||||
const g = demoData.simpleMealGraph();
|
const g = demoData.simpleMealGraph();
|
||||||
g.addEdge(demoData.crabLoopEdge());
|
g.addEdge(demoData.crabLoopEdge());
|
||||||
expect(g.outEdges(demoData.crabNode().address)).toContainEqual(
|
expect(
|
||||||
demoData.crabLoopEdge()
|
g
|
||||||
);
|
.neighborhood(demoData.crabNode().address, {direction: "OUT"})
|
||||||
expect(g.inEdges(demoData.crabNode().address)).toContainEqual(
|
.map(({edge}) => edge)
|
||||||
demoData.crabLoopEdge()
|
).toContainEqual(demoData.crabLoopEdge());
|
||||||
);
|
expect(
|
||||||
|
g
|
||||||
|
.neighborhood(demoData.crabNode().address, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge)
|
||||||
|
).toContainEqual(demoData.crabLoopEdge());
|
||||||
const crabNeighbors = g.neighborhood(demoData.crabNode().address);
|
const crabNeighbors = g.neighborhood(demoData.crabNode().address);
|
||||||
const crabLoops = crabNeighbors.filter(({edge}) =>
|
const crabLoops = crabNeighbors.filter(({edge}) =>
|
||||||
deepEqual(edge, demoData.crabLoopEdge())
|
deepEqual(edge, demoData.crabLoopEdge())
|
||||||
|
@ -315,7 +314,11 @@ describe("graph", () => {
|
||||||
const g = demoData.simpleMealGraph();
|
const g = demoData.simpleMealGraph();
|
||||||
g.addEdge(demoData.duplicateCookEdge());
|
g.addEdge(demoData.duplicateCookEdge());
|
||||||
[demoData.cookEdge(), demoData.duplicateCookEdge()].forEach((e) => {
|
[demoData.cookEdge(), demoData.duplicateCookEdge()].forEach((e) => {
|
||||||
expect(g.outEdges(demoData.mealNode().address)).toContainEqual(e);
|
expect(
|
||||||
|
g
|
||||||
|
.neighborhood(demoData.mealNode().address, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge)
|
||||||
|
).toContainEqual(e);
|
||||||
expect(g.edge(e.address)).toEqual(e);
|
expect(g.edge(e.address)).toEqual(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -381,7 +384,7 @@ describe("graph", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inEdges and outEdges", () => {
|
describe("neighborhood detection", () => {
|
||||||
describe("type filtering", () => {
|
describe("type filtering", () => {
|
||||||
class ExampleGraph {
|
class ExampleGraph {
|
||||||
graph: Graph<{}, {}>;
|
graph: Graph<{}, {}>;
|
||||||
|
@ -441,14 +444,26 @@ describe("graph", () => {
|
||||||
const exampleGraph = new ExampleGraph();
|
const exampleGraph = new ExampleGraph();
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"inEdges",
|
"neighborhood IN",
|
||||||
exampleGraph.inEdges,
|
exampleGraph.inEdges,
|
||||||
(opts) => exampleGraph.graph.inEdges(exampleGraph.root, opts),
|
(opts) =>
|
||||||
|
exampleGraph.graph
|
||||||
|
.neighborhood(exampleGraph.root, {
|
||||||
|
...(opts || {}),
|
||||||
|
direction: "IN",
|
||||||
|
})
|
||||||
|
.map(({edge}) => edge),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"outEdges",
|
"neighborhood OUT",
|
||||||
exampleGraph.outEdges,
|
exampleGraph.outEdges,
|
||||||
(opts) => exampleGraph.graph.outEdges(exampleGraph.root, opts),
|
(opts) =>
|
||||||
|
exampleGraph.graph
|
||||||
|
.neighborhood(exampleGraph.root, {
|
||||||
|
...(opts || {}),
|
||||||
|
direction: "OUT",
|
||||||
|
})
|
||||||
|
.map(({edge}) => edge),
|
||||||
],
|
],
|
||||||
].forEach(([choice, {a1, a2, b1, b2}, edges]) => {
|
].forEach(([choice, {a1, a2, b1, b2}, edges]) => {
|
||||||
describe(choice, () => {
|
describe(choice, () => {
|
||||||
|
@ -516,23 +531,23 @@ describe("graph", () => {
|
||||||
neighborhood: [
|
neighborhood: [
|
||||||
{
|
{
|
||||||
edge: demoData.eatEdge(),
|
edge: demoData.eatEdge(),
|
||||||
neighborAddress: demoData.mealNode().address,
|
neighbor: demoData.mealNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.pickEdge(),
|
edge: demoData.pickEdge(),
|
||||||
neighborAddress: demoData.bananasNode().address,
|
neighbor: demoData.bananasNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.grabEdge(),
|
edge: demoData.grabEdge(),
|
||||||
neighborAddress: demoData.crabNode().address,
|
neighbor: demoData.crabNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.cookEdge(),
|
edge: demoData.cookEdge(),
|
||||||
neighborAddress: demoData.mealNode().address,
|
neighbor: demoData.mealNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.duplicateCookEdge(),
|
edge: demoData.duplicateCookEdge(),
|
||||||
neighborAddress: demoData.mealNode().address,
|
neighbor: demoData.mealNode().address,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -541,11 +556,11 @@ describe("graph", () => {
|
||||||
neighborhood: [
|
neighborhood: [
|
||||||
{
|
{
|
||||||
edge: demoData.pickEdge(),
|
edge: demoData.pickEdge(),
|
||||||
neighborAddress: demoData.heroNode().address,
|
neighbor: demoData.heroNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.bananasIngredientEdge(),
|
edge: demoData.bananasIngredientEdge(),
|
||||||
neighborAddress: demoData.mealNode().address,
|
neighbor: demoData.mealNode().address,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -554,15 +569,15 @@ describe("graph", () => {
|
||||||
neighborhood: [
|
neighborhood: [
|
||||||
{
|
{
|
||||||
edge: demoData.crabIngredientEdge(),
|
edge: demoData.crabIngredientEdge(),
|
||||||
neighborAddress: demoData.mealNode().address,
|
neighbor: demoData.mealNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.grabEdge(),
|
edge: demoData.grabEdge(),
|
||||||
neighborAddress: demoData.heroNode().address,
|
neighbor: demoData.heroNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.crabLoopEdge(),
|
edge: demoData.crabLoopEdge(),
|
||||||
neighborAddress: demoData.crabNode().address,
|
neighbor: demoData.crabNode().address,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -571,23 +586,23 @@ describe("graph", () => {
|
||||||
neighborhood: [
|
neighborhood: [
|
||||||
{
|
{
|
||||||
edge: demoData.bananasIngredientEdge(),
|
edge: demoData.bananasIngredientEdge(),
|
||||||
neighborAddress: demoData.bananasNode().address,
|
neighbor: demoData.bananasNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.crabIngredientEdge(),
|
edge: demoData.crabIngredientEdge(),
|
||||||
neighborAddress: demoData.crabNode().address,
|
neighbor: demoData.crabNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.cookEdge(),
|
edge: demoData.cookEdge(),
|
||||||
neighborAddress: demoData.heroNode().address,
|
neighbor: demoData.heroNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.eatEdge(),
|
edge: demoData.eatEdge(),
|
||||||
neighborAddress: demoData.heroNode().address,
|
neighbor: demoData.heroNode().address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
edge: demoData.duplicateCookEdge(),
|
edge: demoData.duplicateCookEdge(),
|
||||||
neighborAddress: demoData.heroNode().address,
|
neighbor: demoData.heroNode().address,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -617,7 +632,10 @@ describe("graph", () => {
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
|
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
|
||||||
const actual = demoData.advancedMealGraph().outEdges(node.address);
|
const actual = demoData
|
||||||
|
.advancedMealGraph()
|
||||||
|
.neighborhood(node.address, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expectSameSorted(actual, expectedEdges);
|
expectSameSorted(actual, expectedEdges);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -641,7 +659,10 @@ describe("graph", () => {
|
||||||
[demoData.mealNode(), [demoData.eatEdge()]],
|
[demoData.mealNode(), [demoData.eatEdge()]],
|
||||||
];
|
];
|
||||||
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
|
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
|
||||||
const actual = demoData.advancedMealGraph().inEdges(node.address);
|
const actual = demoData
|
||||||
|
.advancedMealGraph()
|
||||||
|
.neighborhood(node.address, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expectSameSorted(actual, expectedEdges);
|
expectSameSorted(actual, expectedEdges);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -649,14 +670,18 @@ describe("graph", () => {
|
||||||
it("gets empty out-edges for a nonexistent node", () => {
|
it("gets empty out-edges for a nonexistent node", () => {
|
||||||
const result = demoData
|
const result = demoData
|
||||||
.simpleMealGraph()
|
.simpleMealGraph()
|
||||||
.outEdges(demoData.makeAddress("hinox", "NPC"));
|
.neighborhood(demoData.makeAddress("hinox", "NPC"), {
|
||||||
|
direction: "OUT",
|
||||||
|
});
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets empty in-edges for a nonexistent node", () => {
|
it("gets empty in-edges for a nonexistent node", () => {
|
||||||
const result = demoData
|
const result = demoData
|
||||||
.simpleMealGraph()
|
.simpleMealGraph()
|
||||||
.inEdges(demoData.makeAddress("hinox", "NPC"));
|
.neighborhood(demoData.makeAddress("hinox", "NPC"), {
|
||||||
|
direction: "IN",
|
||||||
|
});
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -690,7 +715,9 @@ describe("graph", () => {
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.removeNode(danglingSrc().address)
|
.removeNode(danglingSrc().address)
|
||||||
.removeNode(danglingDst().address);
|
.removeNode(danglingDst().address);
|
||||||
const inEdges = g.inEdges(fullyDanglingEdge().dst);
|
const inEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().dst, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -702,7 +729,9 @@ describe("graph", () => {
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.removeNode(danglingSrc().address)
|
.removeNode(danglingSrc().address)
|
||||||
.removeNode(danglingDst().address);
|
.removeNode(danglingDst().address);
|
||||||
const outEdges = g.outEdges(fullyDanglingEdge().src);
|
const outEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().src, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -713,8 +742,10 @@ describe("graph", () => {
|
||||||
.addNode(danglingDst())
|
.addNode(danglingDst())
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.removeEdge(fullyDanglingEdge().address);
|
.removeEdge(fullyDanglingEdge().address);
|
||||||
const outEdges = g.inEdges(fullyDanglingEdge().dst);
|
const inEdges = g.neighborhood(fullyDanglingEdge().dst, {
|
||||||
expect(outEdges).toEqual([]);
|
direction: "IN",
|
||||||
|
});
|
||||||
|
expect(inEdges).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has lack of out-edges for deleted edge", () => {
|
it("has lack of out-edges for deleted edge", () => {
|
||||||
|
@ -724,19 +755,25 @@ describe("graph", () => {
|
||||||
.addNode(danglingDst())
|
.addNode(danglingDst())
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.removeEdge(fullyDanglingEdge().address);
|
.removeEdge(fullyDanglingEdge().address);
|
||||||
const outEdges = g.outEdges(fullyDanglingEdge().src);
|
const outEdges = g.neighborhood(fullyDanglingEdge().src, {
|
||||||
|
direction: "OUT",
|
||||||
|
});
|
||||||
expect(outEdges).toEqual([]);
|
expect(outEdges).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has in-edges for non-existent node with dangling edge", () => {
|
it("has in-edges for non-existent node with dangling edge", () => {
|
||||||
const g = demoData.simpleMealGraph().addEdge(fullyDanglingEdge());
|
const g = demoData.simpleMealGraph().addEdge(fullyDanglingEdge());
|
||||||
const inEdges = g.inEdges(fullyDanglingEdge().dst);
|
const inEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().dst, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has out-edges for non-existent node with dangling edge", () => {
|
it("has out-edges for non-existent node with dangling edge", () => {
|
||||||
const g = demoData.simpleMealGraph().addEdge(fullyDanglingEdge());
|
const g = demoData.simpleMealGraph().addEdge(fullyDanglingEdge());
|
||||||
const outEdges = g.outEdges(fullyDanglingEdge().src);
|
const outEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().src, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -745,7 +782,9 @@ describe("graph", () => {
|
||||||
.simpleMealGraph()
|
.simpleMealGraph()
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.addNode(danglingDst());
|
.addNode(danglingDst());
|
||||||
const inEdges = g.inEdges(fullyDanglingEdge().dst);
|
const inEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().dst, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
expect(inEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -754,7 +793,9 @@ describe("graph", () => {
|
||||||
.simpleMealGraph()
|
.simpleMealGraph()
|
||||||
.addEdge(fullyDanglingEdge())
|
.addEdge(fullyDanglingEdge())
|
||||||
.addNode(danglingSrc());
|
.addNode(danglingSrc());
|
||||||
const outEdges = g.outEdges(fullyDanglingEdge().src);
|
const outEdges = g
|
||||||
|
.neighborhood(fullyDanglingEdge().src, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
expect(outEdges).toEqual([fullyDanglingEdge()]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -777,15 +818,23 @@ describe("graph", () => {
|
||||||
it("is idempotent in terms of in-edges", () => {
|
it("is idempotent in terms of in-edges", () => {
|
||||||
const g1 = originalGraph();
|
const g1 = originalGraph();
|
||||||
const g2 = modifiedGraph();
|
const g2 = modifiedGraph();
|
||||||
const e1 = g1.inEdges(taredge().address);
|
const e1 = g1
|
||||||
const e2 = g2.inEdges(taredge().address);
|
.neighborhood(taredge().address, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
|
const e2 = g2
|
||||||
|
.neighborhood(taredge().address, {direction: "IN"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expectSameSorted(e1, e2);
|
expectSameSorted(e1, e2);
|
||||||
});
|
});
|
||||||
it("is idempotent in terms of out-edges", () => {
|
it("is idempotent in terms of out-edges", () => {
|
||||||
const g1 = originalGraph();
|
const g1 = originalGraph();
|
||||||
const g2 = modifiedGraph();
|
const g2 = modifiedGraph();
|
||||||
const e1 = g1.outEdges(taredge().address);
|
const e1 = g1
|
||||||
const e2 = g2.outEdges(taredge().address);
|
.neighborhood(taredge().address, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
|
const e2 = g2
|
||||||
|
.neighborhood(taredge().address, {direction: "OUT"})
|
||||||
|
.map(({edge}) => edge);
|
||||||
expectSameSorted(e1, e2);
|
expectSameSorted(e1, e2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -848,21 +897,25 @@ describe("graph", () => {
|
||||||
return originalGraph.nodes().map((node) => {
|
return originalGraph.nodes().map((node) => {
|
||||||
const miniGraph = new Graph();
|
const miniGraph = new Graph();
|
||||||
miniGraph.addNode(node);
|
miniGraph.addNode(node);
|
||||||
originalGraph.outEdges(node.address).forEach((edge) => {
|
originalGraph
|
||||||
if (miniGraph.node(edge.dst) === undefined) {
|
.neighborhood(node.address, {direction: "OUT"})
|
||||||
miniGraph.addNode(originalGraph.node(edge.dst));
|
.forEach(({edge}) => {
|
||||||
}
|
if (miniGraph.node(edge.dst) === undefined) {
|
||||||
miniGraph.addEdge(edge);
|
miniGraph.addNode(originalGraph.node(edge.dst));
|
||||||
});
|
}
|
||||||
originalGraph.inEdges(node.address).forEach((edge) => {
|
|
||||||
if (miniGraph.node(edge.src) === undefined) {
|
|
||||||
miniGraph.addNode(originalGraph.node(edge.src));
|
|
||||||
}
|
|
||||||
if (miniGraph.edge(edge.address) === undefined) {
|
|
||||||
// This check is necessary to prevent double-adding loops.
|
|
||||||
miniGraph.addEdge(edge);
|
miniGraph.addEdge(edge);
|
||||||
}
|
});
|
||||||
});
|
originalGraph
|
||||||
|
.neighborhood(node.address, {direction: "IN"})
|
||||||
|
.forEach(({edge}) => {
|
||||||
|
if (miniGraph.node(edge.src) === undefined) {
|
||||||
|
miniGraph.addNode(originalGraph.node(edge.src));
|
||||||
|
}
|
||||||
|
if (miniGraph.edge(edge.address) === undefined) {
|
||||||
|
// This check is necessary to prevent double-adding loops.
|
||||||
|
miniGraph.addEdge(edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
return miniGraph;
|
return miniGraph;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,9 @@ function createTestData(): * {
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {graph, node} = this.props;
|
const {graph, node} = this.props;
|
||||||
const neighborCount = graph.outEdges(node.address).length;
|
const neighborCount = graph.neighborhood(node.address, {
|
||||||
|
direction: "OUT",
|
||||||
|
}).length;
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<tt>{node.address.id}</tt> has neighbor count{" "}
|
<tt>{node.address.id}</tt> has neighbor count{" "}
|
||||||
|
|
|
@ -38,11 +38,12 @@ const adapter: PluginAdapter<NodePayload> = {
|
||||||
// previously queried IDs.)
|
// previously queried IDs.)
|
||||||
function extractParentTitles(node: Node<NodePayload>): string[] {
|
function extractParentTitles(node: Node<NodePayload>): string[] {
|
||||||
return graph
|
return graph
|
||||||
.inEdges(node.address)
|
.neighborhood(node.address, {
|
||||||
.filter((e) => e.address.type === CONTAINS_EDGE_TYPE)
|
direction: "IN",
|
||||||
.map((e) => graph.node(e.src))
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
.map((container) => {
|
})
|
||||||
return adapter.extractTitle(graph, container);
|
.map(({neighbor}) => {
|
||||||
|
return adapter.extractTitle(graph, graph.node(neighbor));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function extractIssueOrPrTitle(
|
function extractIssueOrPrTitle(
|
||||||
|
|
|
@ -62,26 +62,31 @@ describe("createGraph", () => {
|
||||||
id: hash,
|
id: hash,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entryChildren = graph.outEdges(address, {
|
const entryChildren = graph.neighborhood(address, {
|
||||||
nodeType: TREE_ENTRY_NODE_TYPE,
|
nodeType: TREE_ENTRY_NODE_TYPE,
|
||||||
edgeType: INCLUDES_EDGE_TYPE,
|
edgeType: INCLUDES_EDGE_TYPE,
|
||||||
|
direction: "OUT",
|
||||||
});
|
});
|
||||||
expect(entryChildren).toHaveLength(
|
expect(entryChildren).toHaveLength(
|
||||||
Object.keys(data.trees[hash].entries).length
|
Object.keys(data.trees[hash].entries).length
|
||||||
);
|
);
|
||||||
expect(graph.outEdges(address)).toHaveLength(entryChildren.length);
|
expect(graph.neighborhood(address, {direction: "OUT"})).toHaveLength(
|
||||||
|
entryChildren.length
|
||||||
|
);
|
||||||
|
|
||||||
expect(graph.node(address)).toEqual({address, payload: {}});
|
expect(graph.node(address)).toEqual({address, payload: {}});
|
||||||
const owningCommits = graph.inEdges(address, {
|
const owningCommits = graph.neighborhood(address, {
|
||||||
nodeType: COMMIT_NODE_TYPE,
|
nodeType: COMMIT_NODE_TYPE,
|
||||||
edgeType: HAS_TREE_EDGE_TYPE,
|
edgeType: HAS_TREE_EDGE_TYPE,
|
||||||
|
direction: "IN",
|
||||||
});
|
});
|
||||||
expect(owningCommits.length).toBeLessThanOrEqual(1);
|
expect(owningCommits.length).toBeLessThanOrEqual(1);
|
||||||
const parentTreeEntries = graph.inEdges(address, {
|
const parentTreeEntries = graph.neighborhood(address, {
|
||||||
nodeType: TREE_ENTRY_NODE_TYPE,
|
nodeType: TREE_ENTRY_NODE_TYPE,
|
||||||
edgeType: HAS_CONTENTS_EDGE_TYPE,
|
edgeType: HAS_CONTENTS_EDGE_TYPE,
|
||||||
|
direction: "IN",
|
||||||
});
|
});
|
||||||
expect(graph.inEdges(address)).toHaveLength(
|
expect(graph.neighborhood(address, {direction: "IN"})).toHaveLength(
|
||||||
owningCommits.length + parentTreeEntries.length
|
owningCommits.length + parentTreeEntries.length
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -110,14 +115,18 @@ describe("createGraph", () => {
|
||||||
id: treeEntryId(hash, name),
|
id: treeEntryId(hash, name),
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
graph.inEdges(entryAddress, {
|
graph.neighborhood(entryAddress, {
|
||||||
nodeType: TREE_NODE_TYPE,
|
nodeType: TREE_NODE_TYPE,
|
||||||
edgeType: INCLUDES_EDGE_TYPE,
|
edgeType: INCLUDES_EDGE_TYPE,
|
||||||
|
direction: "IN",
|
||||||
})
|
})
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
const shouldHaveContents = tree.entries[name].type !== "commit";
|
const shouldHaveContents = tree.entries[name].type !== "commit";
|
||||||
expect(
|
expect(
|
||||||
graph.outEdges(entryAddress, {edgeType: HAS_CONTENTS_EDGE_TYPE})
|
graph.neighborhood(entryAddress, {
|
||||||
|
edgeType: HAS_CONTENTS_EDGE_TYPE,
|
||||||
|
direction: "OUT",
|
||||||
|
})
|
||||||
).toHaveLength(shouldHaveContents ? 1 : 0);
|
).toHaveLength(shouldHaveContents ? 1 : 0);
|
||||||
expect(graph.neighborhood(entryAddress)).toHaveLength(
|
expect(graph.neighborhood(entryAddress)).toHaveLength(
|
||||||
shouldHaveContents ? 2 : 1
|
shouldHaveContents ? 2 : 1
|
||||||
|
@ -142,7 +151,7 @@ describe("createGraph", () => {
|
||||||
.neighborhood(nodeAddress, filter)
|
.neighborhood(nodeAddress, filter)
|
||||||
.filter((x) => predicate(x));
|
.filter((x) => predicate(x));
|
||||||
expect(edges).toHaveLength(1);
|
expect(edges).toHaveLength(1);
|
||||||
return edges[0].neighborAddress;
|
return edges[0].neighbor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueTree(graph, commitAddress) {
|
function uniqueTree(graph, commitAddress) {
|
||||||
|
@ -218,7 +227,9 @@ describe("createGraph", () => {
|
||||||
expect(graph.node(treeEntryAddress)).toEqual(expect.anything());
|
expect(graph.node(treeEntryAddress)).toEqual(expect.anything());
|
||||||
// Submodule commits never have contents, because the commit nodes
|
// Submodule commits never have contents, because the commit nodes
|
||||||
// are from an unknown repository.
|
// are from an unknown repository.
|
||||||
expect(graph.outEdges(treeEntryAddress)).toHaveLength(0);
|
expect(
|
||||||
|
graph.neighborhood(treeEntryAddress, {direction: "OUT"})
|
||||||
|
).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -113,7 +113,7 @@ class Post<
|
||||||
edgeType: AUTHORS_EDGE_TYPE,
|
edgeType: AUTHORS_EDGE_TYPE,
|
||||||
nodeType: AUTHOR_NODE_TYPE,
|
nodeType: AUTHOR_NODE_TYPE,
|
||||||
})
|
})
|
||||||
.map(({neighborAddress}) => new Author(this.graph, neighborAddress));
|
.map(({neighbor}) => new Author(this.graph, neighbor));
|
||||||
}
|
}
|
||||||
|
|
||||||
body(): string {
|
body(): string {
|
||||||
|
@ -130,7 +130,7 @@ class Commentable<T: IssueNodePayload | PullRequestNodePayload> extends Post<
|
||||||
edgeType: CONTAINS_EDGE_TYPE,
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
nodeType: COMMENT_NODE_TYPE,
|
nodeType: COMMENT_NODE_TYPE,
|
||||||
})
|
})
|
||||||
.map(({neighborAddress}) => new Comment(this.graph, neighborAddress));
|
.map(({neighbor}) => new Comment(this.graph, neighbor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,14 +38,16 @@ describe("GithubParser", () => {
|
||||||
.filter((n) => n.address.type === "COMMENT");
|
.filter((n) => n.address.type === "COMMENT");
|
||||||
expect(comments).not.toHaveLength(0);
|
expect(comments).not.toHaveLength(0);
|
||||||
comments.forEach((c) => {
|
comments.forEach((c) => {
|
||||||
const authorEdges = graph
|
const authorNeighbors = graph.neighborhood(c.address, {
|
||||||
.outEdges(c.address)
|
edgeType: AUTHORS_EDGE_TYPE,
|
||||||
.filter((e) => e.address.type === AUTHORS_EDGE_TYPE);
|
direction: "OUT",
|
||||||
expect(authorEdges.length).toBe(1);
|
});
|
||||||
const containerEdges = graph
|
expect(authorNeighbors.length).toBe(1);
|
||||||
.inEdges(c.address)
|
const containerNeighbors = graph.neighborhood(c.address, {
|
||||||
.filter((e) => e.address.type === CONTAINS_EDGE_TYPE);
|
direction: "IN",
|
||||||
expect(containerEdges.length).toBe(1);
|
edgeType: CONTAINS_EDGE_TYPE,
|
||||||
|
});
|
||||||
|
expect(containerNeighbors.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,11 +59,11 @@ describe("GithubParser", () => {
|
||||||
);
|
);
|
||||||
expect(issuesAndPRs).not.toHaveLength(0);
|
expect(issuesAndPRs).not.toHaveLength(0);
|
||||||
issuesAndPRs.forEach((x) => {
|
issuesAndPRs.forEach((x) => {
|
||||||
const outEdges = graph.outEdges(x.address);
|
const authorNeighbors = graph.neighborhood(x.address, {
|
||||||
const authorEdges = outEdges.filter(
|
edgeType: AUTHORS_EDGE_TYPE,
|
||||||
(e) => e.address.type === AUTHORS_EDGE_TYPE
|
direction: "OUT",
|
||||||
);
|
});
|
||||||
expect(authorEdges.length).toBe(1);
|
expect(authorNeighbors.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue