From dd83d7b4abdbaab1e44aa0474eecafc180d279ce Mon Sep 17 00:00:00 2001 From: William Chargin Date: Tue, 26 Jun 2018 14:00:19 -0700 Subject: [PATCH] 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 --- src/v3/plugins/git/createGraph.test.js | 6 + src/v3/plugins/git/graphView.js | 327 +++++++++++++++++++ src/v3/plugins/git/graphView.test.js | 427 +++++++++++++++++++++++++ 3 files changed, 760 insertions(+) create mode 100644 src/v3/plugins/git/graphView.js create mode 100644 src/v3/plugins/git/graphView.test.js diff --git a/src/v3/plugins/git/createGraph.test.js b/src/v3/plugins/git/createGraph.test.js index 56fff1b..17b727f 100644 --- a/src/v3/plugins/git/createGraph.test.js +++ b/src/v3/plugins/git/createGraph.test.js @@ -8,6 +8,7 @@ import { findBecomesEdges, findBecomesEdgesForCommits, } from "./createGraph"; +import {GraphView} from "./graphView"; import type {Hash, Tree} from "./types"; const makeData = () => cloneDeep(require("./demoData/example-git")); @@ -17,6 +18,11 @@ describe("plugins/git/createGraph", () => { it("processes a simple repository", () => { expect(createGraph(makeData())).toMatchSnapshot(); }); + + it("satisfies the GraphView invariants", () => { + const graph = createGraph(makeData()); + expect(() => new GraphView(graph)).not.toThrow(); + }); }); describe("findBecomesEdgesForCommits", () => { diff --git a/src/v3/plugins/git/graphView.js b/src/v3/plugins/git/graphView.js new file mode 100644 index 0000000..bd4c882 --- /dev/null +++ b/src/v3/plugins/git/graphView.js @@ -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( + node: GN.StructuredAddress, + options: NeighborsOptions + ): Iterator { + 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 { + const result = this._commits(); + this._maybeCheckInvariants(); + return result; + } + + *_commits(): Iterator { + 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 { + const result: Iterator = this._neighbors(commit, { + direction: Direction.OUT, + nodePrefix: GN._Prefix.commit, + edgePrefix: GE._Prefix.hasParent, + }); + this._maybeCheckInvariants(); + return result; + } + + entries(tree: GN.TreeAddress): Iterator { + const result: Iterator = this._neighbors(tree, { + direction: Direction.OUT, + nodePrefix: GN._Prefix.treeEntry, + edgePrefix: GE._Prefix.includes, + }); + this._maybeCheckInvariants(); + return result; + } + + contents(entry: GN.TreeEntryAddress): Iterator { + const result: Iterator = 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 { + const result: Iterator = this._neighbors(entry, { + direction: Direction.OUT, + nodePrefix: GN._Prefix.treeEntry, + edgePrefix: GE._Prefix.becomes, + }); + this._maybeCheckInvariants(); + return result; + } + + evolvesFrom(entry: GN.TreeEntryAddress): Iterator { + const result: Iterator = 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)}` + ); + } + } + } +} diff --git a/src/v3/plugins/git/graphView.test.js b/src/v3/plugins/git/graphView.test.js new file mode 100644 index 0000000..41e72e2 --- /dev/null +++ b/src/v3/plugins/git/graphView.test.js @@ -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, y: Iterable) { + 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 = [ + { + type: GN.BLOB_TYPE, + hash: "f1f2514ca6d7a6a1a0511957021b1995bf9ace1c", + }, + ]; + expect(actual).toEqual(expected); + }); + it("for trees", () => { + const actual = Array.from(view.contents(entryByName("src"))); + const expected: $ReadOnlyArray = [ + { + 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 = [ + { + 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) + ); + }); + }); + }); + }); +});