Implement a Git graph view (#415)

Summary:
Similar in structure to the GitHub graph view.

Test Plan:
Unit tests added, with full coverage.

wchargin-branch: git-graph-view
This commit is contained in:
William Chargin 2018-06-26 14:00:19 -07:00 committed by GitHub
parent 0522894a8d
commit dd83d7b4ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 760 additions and 0 deletions

View File

@ -8,6 +8,7 @@ import {
findBecomesEdges, findBecomesEdges,
findBecomesEdgesForCommits, findBecomesEdgesForCommits,
} from "./createGraph"; } from "./createGraph";
import {GraphView} from "./graphView";
import type {Hash, Tree} from "./types"; import type {Hash, Tree} from "./types";
const makeData = () => cloneDeep(require("./demoData/example-git")); const makeData = () => cloneDeep(require("./demoData/example-git"));
@ -17,6 +18,11 @@ describe("plugins/git/createGraph", () => {
it("processes a simple repository", () => { it("processes a simple repository", () => {
expect(createGraph(makeData())).toMatchSnapshot(); expect(createGraph(makeData())).toMatchSnapshot();
}); });
it("satisfies the GraphView invariants", () => {
const graph = createGraph(makeData());
expect(() => new GraphView(graph)).not.toThrow();
});
}); });
describe("findBecomesEdgesForCommits", () => { describe("findBecomesEdgesForCommits", () => {

View File

@ -0,0 +1,327 @@
// @flow
import {
type EdgeAddressT,
type NeighborsOptions,
type NodeAddressT,
Direction,
Graph,
NodeAddress,
edgeToString,
} from "../../core/graph";
import * as GN from "./nodes";
import * as GE from "./edges";
export class GraphView {
_graph: Graph;
constructor(graph: Graph): void {
this._graph = graph;
this._maybeCheckInvariants();
}
*_neighbors<T: GN.StructuredAddress>(
node: GN.StructuredAddress,
options: NeighborsOptions
): Iterator<T> {
if (!NodeAddress.hasPrefix(options.nodePrefix, GN._Prefix.base)) {
throw new Error(`_neighbors must filter to Git nodes`);
}
const rawNode: GN.RawAddress = GN.toRaw(node);
for (const neighbor of this._graph.neighbors(rawNode, options)) {
this._maybeCheckInvariants();
yield ((GN.fromRaw(
(((neighbor.node: NodeAddressT): any): GN.RawAddress)
): any): T);
}
this._maybeCheckInvariants();
}
graph(): Graph {
const result = this._graph;
this._maybeCheckInvariants();
return result;
}
commits(): Iterator<GN.CommitAddress> {
const result = this._commits();
this._maybeCheckInvariants();
return result;
}
*_commits(): Iterator<GN.CommitAddress> {
for (const node of this._graph.nodes({prefix: GN._Prefix.commit})) {
const rawAddress: GN.RawAddress = ((node: NodeAddressT): any);
const commit: GN.CommitAddress = (GN.fromRaw(rawAddress): any);
this._maybeCheckInvariants();
yield commit;
}
this._maybeCheckInvariants();
}
tree(commit: GN.CommitAddress): GN.TreeAddress {
const result: GN.TreeAddress = Array.from(
this._neighbors(commit, {
direction: Direction.OUT,
nodePrefix: GN._Prefix.tree,
edgePrefix: GE._Prefix.hasTree,
})
)[0];
this._maybeCheckInvariants();
return result;
}
parents(commit: GN.CommitAddress): Iterator<GN.CommitAddress> {
const result: Iterator<GN.CommitAddress> = this._neighbors(commit, {
direction: Direction.OUT,
nodePrefix: GN._Prefix.commit,
edgePrefix: GE._Prefix.hasParent,
});
this._maybeCheckInvariants();
return result;
}
entries(tree: GN.TreeAddress): Iterator<GN.TreeEntryAddress> {
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(tree, {
direction: Direction.OUT,
nodePrefix: GN._Prefix.treeEntry,
edgePrefix: GE._Prefix.includes,
});
this._maybeCheckInvariants();
return result;
}
contents(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryContentsAddress> {
const result: Iterator<GN.TreeEntryContentsAddress> = this._neighbors(
entry,
{
direction: Direction.OUT,
nodePrefix: GN._Prefix.base, // multiple kinds
edgePrefix: GE._Prefix.hasContents,
}
);
this._maybeCheckInvariants();
return result;
}
evolvesTo(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryAddress> {
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(entry, {
direction: Direction.OUT,
nodePrefix: GN._Prefix.treeEntry,
edgePrefix: GE._Prefix.becomes,
});
this._maybeCheckInvariants();
return result;
}
evolvesFrom(entry: GN.TreeEntryAddress): Iterator<GN.TreeEntryAddress> {
const result: Iterator<GN.TreeEntryAddress> = this._neighbors(entry, {
direction: Direction.IN,
nodePrefix: GN._Prefix.treeEntry,
edgePrefix: GE._Prefix.becomes,
});
this._maybeCheckInvariants();
return result;
}
_maybeCheckInvariants() {
if (process.env.NODE_ENV === "test") {
// TODO(perf): If this method becomes really slow, we can disable
// it on specific tests wherein we construct large graphs.
this.checkInvariants();
}
}
checkInvariants() {
// All Git nodes and edges must have valid Git addresses.
for (const node of this._graph.nodes({prefix: GN._Prefix.base})) {
GN.fromRaw((((node: NodeAddressT): any): GN.RawAddress));
}
// (Edges are checked down below.)
// All Git edges must have `src` and `dst` of specific types.
type EdgeInvariant = {|
+prefix: GE.RawAddress,
+homs: $ReadOnlyArray<{|
+srcPrefix: NodeAddressT,
+dstPrefix: NodeAddressT,
|}>,
|};
const edgeInvariants = {
[GE.HAS_TREE_TYPE]: {
prefix: GE._Prefix.hasTree,
homs: [{srcPrefix: GN._Prefix.commit, dstPrefix: GN._Prefix.tree}],
},
[GE.HAS_PARENT_TYPE]: {
prefix: GE._Prefix.hasParent,
homs: [{srcPrefix: GN._Prefix.commit, dstPrefix: GN._Prefix.commit}],
},
[GE.INCLUDES_TYPE]: {
prefix: GE._Prefix.includes,
homs: [{srcPrefix: GN._Prefix.tree, dstPrefix: GN._Prefix.treeEntry}],
},
[GE.BECOMES_TYPE]: {
prefix: GE._Prefix.becomes,
homs: [
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.treeEntry},
],
},
[GE.HAS_CONTENTS_TYPE]: {
prefix: GE._Prefix.hasContents,
homs: [
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.blob},
{srcPrefix: GN._Prefix.treeEntry, dstPrefix: GN._Prefix.tree},
{
srcPrefix: GN._Prefix.treeEntry,
dstPrefix: GN._Prefix.submoduleCommit,
},
],
},
};
for (const edge of this._graph.edges({
addressPrefix: GE._Prefix.base,
srcPrefix: NodeAddress.empty,
dstPrefix: NodeAddress.empty,
})) {
const address = GE.fromRaw(
(((edge.address: EdgeAddressT): any): GE.RawAddress)
);
const invariant: EdgeInvariant = edgeInvariants[address.type];
if (invariant == null) {
throw new Error(
`Missing invariant definition for: ${String(address.type)}`
);
}
if (
!invariant.homs.some(
({srcPrefix, dstPrefix}) =>
NodeAddress.hasPrefix(edge.src, srcPrefix) &&
NodeAddress.hasPrefix(edge.dst, dstPrefix)
)
) {
throw new Error(`invariant violation: bad hom: ${edgeToString(edge)}`);
}
}
// Each commits must have a unique and properly named HAS_TREE edge.
for (const rawNode of this._graph.nodes({prefix: GN._Prefix.commit})) {
const treeNeighbors = Array.from(
this._graph.neighbors(rawNode, {
direction: Direction.OUT,
nodePrefix: NodeAddress.empty,
edgePrefix: GE._Prefix.hasTree,
})
);
if (treeNeighbors.length !== 1) {
throw new Error(
"invariant violation: commit should have 1 tree, " +
`but has ${treeNeighbors.length}: ${NodeAddress.toString(rawNode)}`
);
}
const rawEdge = treeNeighbors[0].edge;
const edge: GE.HasTreeAddress = (GE.fromRaw(
(((rawEdge.address: EdgeAddressT): any): GE.RawAddress)
): any);
const node: GN.CommitAddress = ((GN.fromRaw(
(((rawNode: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
if (node.hash !== edge.commit.hash) {
throw new Error(
`invariant violation: bad HAS_TREE edge: ${edgeToString(rawEdge)}`
);
}
}
// All HAS_PARENT edges must map between between the correct commits.
for (const edge of this._graph.edges({
addressPrefix: GE._Prefix.hasParent,
srcPrefix: NodeAddress.empty,
dstPrefix: NodeAddress.empty,
})) {
const src: GN.CommitAddress = ((GN.fromRaw(
(((edge.src: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const dst: GN.CommitAddress = ((GN.fromRaw(
(((edge.dst: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const expectedEdge = GE.createEdge.hasParent(src, dst);
if (edge.address !== expectedEdge.address) {
throw new Error(
`invariant violation: bad HAS_PARENT edge: ${edgeToString(edge)}`
);
}
}
// Each tree entry must have a unique and properly named INCLUDES edge.
for (const rawNode of this._graph.nodes({prefix: GN._Prefix.treeEntry})) {
const treeNeighbors = Array.from(
this._graph.neighbors(rawNode, {
direction: Direction.IN,
nodePrefix: NodeAddress.empty,
edgePrefix: GE._Prefix.includes,
})
);
if (treeNeighbors.length !== 1) {
throw new Error(
"invariant violation: tree entry should have 1 inclusion, " +
`but has ${treeNeighbors.length}: ${NodeAddress.toString(rawNode)}`
);
}
const edge = treeNeighbors[0].edge;
const tree: GN.TreeAddress = ((GN.fromRaw(
(((edge.src: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const treeEntry: GN.TreeEntryAddress = ((GN.fromRaw(
(((edge.dst: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const expectedEdge = GE.createEdge.includes(tree, treeEntry);
if (edge.address !== expectedEdge.address) {
throw new Error(
`invariant violation: bad INCLUDES edge: ${edgeToString(edge)}`
);
}
}
// All BECOMES edges must map between between the correct tree entries.
for (const edge of this._graph.edges({
addressPrefix: GE._Prefix.becomes,
srcPrefix: NodeAddress.empty,
dstPrefix: NodeAddress.empty,
})) {
const src: GN.TreeEntryAddress = ((GN.fromRaw(
(((edge.src: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const dst: GN.TreeEntryAddress = ((GN.fromRaw(
(((edge.dst: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const expectedEdge = GE.createEdge.becomes(src, dst);
if (edge.address !== expectedEdge.address) {
throw new Error(
`invariant violation: bad BECOMES edge: ${edgeToString(edge)}`
);
}
}
// All HAS_CONTENTS edges must be properly named.
for (const edge of this._graph.edges({
addressPrefix: GE._Prefix.hasContents,
srcPrefix: NodeAddress.empty,
dstPrefix: NodeAddress.empty,
})) {
const src: GN.TreeEntryAddress = ((GN.fromRaw(
(((edge.src: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const dst: GN.TreeEntryContentsAddress = ((GN.fromRaw(
(((edge.dst: NodeAddressT): any): GN.RawAddress)
): GN.StructuredAddress): any);
const expectedEdge = GE.createEdge.hasContents(src, dst);
if (edge.address !== expectedEdge.address) {
throw new Error(
`invariant violation: bad HAS_CONTENTS edge: ${edgeToString(edge)}`
);
}
}
}
}

View File

@ -0,0 +1,427 @@
// @flow
import cloneDeep from "lodash.clonedeep";
import {EdgeAddress, Graph, NodeAddress, edgeToString} from "../../core/graph";
import {createGraph} from "./createGraph";
import {GraphView} from "./graphView";
import type {Repository} from "./types";
import * as GE from "./edges";
import * as GN from "./nodes";
const makeData = (): Repository => cloneDeep(require("./demoData/example-git"));
const makeGraph = () => createGraph(makeData());
const makeView = () => new GraphView(makeGraph());
describe("plugins/git/graphView", () => {
const view = makeView();
function expectEqualMultisets(x: Iterable<mixed>, y: Iterable<mixed>) {
const ax = Array.from(x);
const ay = Array.from(y);
expect(ax).toEqual(expect.arrayContaining(ay));
expect(ay).toEqual(expect.arrayContaining(ax));
}
describe("GraphView", () => {
it("#graph returns the provided graph", () => {
const g1 = new Graph();
const g2 = makeGraph();
expect(new GraphView(g1).graph()).toBe(g1);
expect(new GraphView(g2).graph()).toBe(g2);
});
it("#commits yields all commits", () => {
const expectedHashes = Object.keys(makeData().commits);
const actualHashes = Array.from(view.commits()).map((a) => a.hash);
expectEqualMultisets(actualHashes, expectedHashes);
});
it("#tree yields the correct tree for each commit", () => {
const commits = makeData().commits;
for (const commitHash of Object.keys(commits)) {
const commit = commits[commitHash];
const node: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: commitHash};
expect(view.tree(node).hash).toEqual(commit.treeHash);
}
});
it("#parents yields the correct parents for each commit", () => {
const commits = makeData().commits;
expect(Object.keys(commits)).not.toEqual([]);
for (const commitHash of Object.keys(commits)) {
const commit = commits[commitHash];
const node: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: commitHash};
const expectedParents = commit.parentHashes.slice();
const actualParents = Array.from(view.parents(node)).map((a) => a.hash);
expectEqualMultisets(actualParents, expectedParents);
}
});
it("#entries yields the correct entries for each tree", () => {
const trees = makeData().trees;
expect(Object.keys(trees)).not.toEqual([]);
for (const treeHash of Object.keys(trees)) {
const tree = trees[treeHash];
const node: GN.TreeAddress = {type: GN.TREE_TYPE, hash: treeHash};
const actualEntries = Array.from(view.entries(node));
const expectedEntries = Object.keys(tree.entries).map((name) => ({
type: "TREE_ENTRY",
treeHash,
name,
}));
expectEqualMultisets(actualEntries, expectedEntries);
}
});
describe("#contents yields the correct contents", () => {
const entryByName = (name): GN.TreeEntryAddress => ({
type: GN.TREE_ENTRY_TYPE,
treeHash: "7be3ecfee5314ffa9b2d93fc4377792b2d6d70ed",
name,
});
it("for blobs", () => {
const actual = Array.from(view.contents(entryByName("science.txt")));
const expected: $ReadOnlyArray<GN.BlobAddress> = [
{
type: GN.BLOB_TYPE,
hash: "f1f2514ca6d7a6a1a0511957021b1995bf9ace1c",
},
];
expect(actual).toEqual(expected);
});
it("for trees", () => {
const actual = Array.from(view.contents(entryByName("src")));
const expected: $ReadOnlyArray<GN.TreeAddress> = [
{
type: GN.TREE_TYPE,
hash: "78fc9c83023386854c6bfdc5761c0e58f68e226f",
},
];
expect(actual).toEqual(expected);
});
it("for submodule commits", () => {
const actual = Array.from(
view.contents(entryByName("pygravitydefier"))
);
const expected: $ReadOnlyArray<GN.SubmoduleCommitAddress> = [
{
type: GN.SUBMODULE_COMMIT_TYPE,
submoduleUrl:
"https://github.com/sourcecred/example-git-submodule.git",
commitHash: "29ef158bc982733e2ba429fcf73e2f7562244188",
},
];
expect(actual).toEqual(expected);
});
});
it("#evolvesTo yields the correct entries", () => {
const v0: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "569e1d383759903134df75230d63c0090196d4cb",
name: "pygravitydefier",
};
const v1: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "819fc546cea489476ce8dc90785e9ba7753d0a8f",
name: "pygravitydefier",
};
expect(Array.from(view.evolvesTo(v0))).toEqual([v1]);
});
it("#evolvesFrom yields the correct entries", () => {
const v0: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "569e1d383759903134df75230d63c0090196d4cb",
name: "pygravitydefier",
};
const v1: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "819fc546cea489476ce8dc90785e9ba7753d0a8f",
name: "pygravitydefier",
};
expect(Array.from(view.evolvesFrom(v1))).toEqual([v0]);
});
describe("invariants", () => {
it("check for malformed nodes", () => {
const node = GN._gitAddress("wat");
const g = new Graph().addNode(node);
const expected = "Bad address: " + NodeAddress.toString(node);
expect(() => new GraphView(g)).toThrow(expected);
});
it("check for malformed edges", () => {
const c1: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
const c2: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c2"};
const edge = {
address: EdgeAddress.append(GE._Prefix.base, "wat"),
src: GN.toRaw(c1),
dst: GN.toRaw(c2),
};
const g = new Graph()
.addNode(GN.toRaw(c1))
.addNode(GN.toRaw(c2))
.addEdge(edge);
const expected = "Bad address: " + EdgeAddress.toString(edge.address);
expect(() => new GraphView(g)).toThrow(expected);
});
describe("check HAS_TREE edges", () => {
const commit: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
const otherCommit: GN.CommitAddress = {
type: GN.COMMIT_TYPE,
hash: "c2",
};
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
const otherTree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t2"};
const edge = GE.createEdge.hasTree(commit, tree);
const otherEdge = GE.createEdge.hasTree(otherCommit, otherTree);
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
const baseGraph = () =>
new Graph()
.addNode(foreignNode)
.addNode(GN.toRaw(commit))
.addNode(GN.toRaw(tree))
.addNode(GN.toRaw(otherTree));
it("for proper src", () => {
const badEdge = {...edge, src: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for proper dst", () => {
const badEdge = {...edge, dst: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for existence", () => {
const g = baseGraph();
expect(() => new GraphView(g)).toThrow(
"invariant violation: commit should have 1 tree, but has 0: " +
NodeAddress.toString(GN.toRaw(commit))
);
});
it("for correctness", () => {
const badEdge = {...otherEdge, src: edge.src};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad HAS_TREE edge: " + edgeToString(badEdge)
);
});
});
describe("check HAS_PARENT edges", () => {
const c1: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c1"};
const c2: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c2"};
const c3: GN.CommitAddress = {type: GN.COMMIT_TYPE, hash: "c3"};
const e2 = GE.createEdge.hasParent(c1, c2);
const e3 = GE.createEdge.hasParent(c1, c3);
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
const baseGraph = () =>
new Graph()
.addNode(foreignNode)
.addNode(GN.toRaw(c1))
.addNode(GN.toRaw(c2))
.addNode(GN.toRaw(c3))
.addNode(GN.toRaw(tree))
.addEdge(GE.createEdge.hasTree(c1, tree))
.addEdge(GE.createEdge.hasTree(c2, tree))
.addEdge(GE.createEdge.hasTree(c3, tree));
it("for proper src", () => {
const badEdge = {...e2, src: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for proper dst", () => {
const badEdge = {...e2, dst: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for correctness", () => {
const badEdge = {...e2, src: GN.toRaw(c2), dst: GN.toRaw(c3)};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad HAS_PARENT edge: " + edgeToString(badEdge)
);
});
it("allowing multiple parents", () => {
const g = baseGraph()
.addEdge(e2)
.addEdge(e3);
expect(() => new GraphView(g)).not.toThrow();
});
});
describe("check INCLUDES edges", () => {
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
const entry: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: tree.hash,
name: "tree_entry.txt",
};
const anotherEntry: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: tree.hash,
name: "wat.txt",
};
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
const edge = GE.createEdge.includes(tree, entry);
const baseGraph = () =>
new Graph()
.addNode(foreignNode)
.addNode(GN.toRaw(tree))
.addNode(GN.toRaw(entry));
it("for proper src", () => {
const badEdge = {...edge, src: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for proper dst", () => {
const badEdge = {...edge, dst: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for existence", () => {
const g = baseGraph();
expect(() => new GraphView(g)).toThrow(
"invariant violation: " +
"tree entry should have 1 inclusion, but has 0: " +
NodeAddress.toString(GN.toRaw(entry))
);
});
it("for correctness", () => {
const badEdge = {
...edge,
address: GE.createEdge.includes(tree, anotherEntry).address,
};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad INCLUDES edge: " + edgeToString(badEdge)
);
});
});
describe("check BECOMES edges", () => {
const t1: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
const t2: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "t1"};
const te1: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "t1",
name: "foo",
};
const te2: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "t2",
name: "foo",
};
const te3: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "t2",
name: "bar",
};
const e2 = GE.createEdge.becomes(te1, te2);
const e3 = GE.createEdge.becomes(te1, te3);
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
const baseGraph = () =>
new Graph()
.addNode(foreignNode)
.addNode(GN.toRaw(t1))
.addNode(GN.toRaw(t2))
.addNode(GN.toRaw(te1))
.addNode(GN.toRaw(te2))
.addNode(GN.toRaw(te3))
.addEdge(GE.createEdge.includes(t1, te1))
.addEdge(GE.createEdge.includes(t2, te2))
.addEdge(GE.createEdge.includes(t2, te3));
it("for proper src", () => {
const badEdge = {...e2, src: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for proper dst", () => {
const badEdge = {...e2, dst: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for correctness", () => {
const badEdge = {...e2, src: GN.toRaw(te2), dst: GN.toRaw(te3)};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad BECOMES edge: " + edgeToString(badEdge)
);
});
it("allowing multiple edges from a single source", () => {
const g = baseGraph()
.addEdge(e2)
.addEdge(e3);
expect(() => new GraphView(g)).not.toThrow();
});
});
describe("checks HAS_CONTENTS edges", () => {
const tree: GN.TreeAddress = {type: GN.TREE_TYPE, hash: "ceda12"};
const te1: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "ceda12",
name: "foo",
};
const te2: GN.TreeEntryAddress = {
type: GN.TREE_ENTRY_TYPE,
treeHash: "ceda12",
name: "bar",
};
const blob: GN.BlobAddress = {type: GN.BLOB_TYPE, hash: "fish"};
const e1 = GE.createEdge.hasContents(te1, blob);
const foreignNode = NodeAddress.fromParts(["who", "are", "you"]);
const baseGraph = () =>
new Graph()
.addNode(foreignNode)
.addNode(GN.toRaw(tree))
.addNode(GN.toRaw(te1))
.addNode(GN.toRaw(te2))
.addNode(GN.toRaw(blob))
.addEdge(GE.createEdge.includes(tree, te1))
.addEdge(GE.createEdge.includes(tree, te2));
it("for proper src", () => {
const badEdge = {...e1, src: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for proper dst", () => {
const badEdge = {...e1, dst: foreignNode};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad hom: " + edgeToString(badEdge)
);
});
it("for correctness", () => {
const badEdge = {...e1, src: GN.toRaw(te2)};
const g = baseGraph().addEdge(badEdge);
expect(() => new GraphView(g)).toThrow(
"invariant violation: bad HAS_CONTENTS edge: " +
edgeToString(badEdge)
);
});
});
});
});
});