diff --git a/src/plugins/git/createGraph.test.js b/src/plugins/git/createGraph.test.js index 4b6fd4d..7877390 100644 --- a/src/plugins/git/createGraph.test.js +++ b/src/plugins/git/createGraph.test.js @@ -3,7 +3,6 @@ import cloneDeep from "lodash.clonedeep"; import {createGraph} from "./createGraph"; -import {GraphView} from "./graphView"; import {Prefix as NodePrefix} from "./nodes"; import {Prefix as EdgePrefix} from "./edges"; import {NodeAddress, EdgeAddress} from "../../core/graph"; @@ -16,11 +15,6 @@ describe("plugins/git/createGraph", () => { expect(createGraph(makeData())).toMatchSnapshot(); }); - it("satisfies the GraphView invariants", () => { - const graph = createGraph(makeData()); - expect(() => new GraphView(graph)).not.toThrow(); - }); - it("only has commit nodes and has_parent edges", () => { const graph = createGraph(makeData()); for (const n of graph.nodes()) { diff --git a/src/plugins/git/graphView.js b/src/plugins/git/graphView.js deleted file mode 100644 index df1dae4..0000000 --- a/src/plugins/git/graphView.js +++ /dev/null @@ -1,148 +0,0 @@ -// @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(); - } - - 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; - } - - _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_PARENT_TYPE]: { - prefix: GE.Prefix.hasParent, - homs: [{srcPrefix: GN.Prefix.commit, dstPrefix: GN.Prefix.commit}], - }, - }; - - 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)}`); - } - } - - // 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)}` - ); - } - } - } -} diff --git a/src/plugins/git/graphView.test.js b/src/plugins/git/graphView.test.js deleted file mode 100644 index 12baf9c..0000000 --- a/src/plugins/git/graphView.test.js +++ /dev/null @@ -1,118 +0,0 @@ -// @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("./example/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("#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); - } - }); - - 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_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 foreignNode = NodeAddress.fromParts(["who", "are", "you"]); - const baseGraph = () => - new Graph() - .addNode(foreignNode) - .addNode(GN.toRaw(c1)) - .addNode(GN.toRaw(c2)) - .addNode(GN.toRaw(c3)); - 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(); - }); - }); - }); - }); -}); diff --git a/src/plugins/github/__snapshots__/graphView.test.js.snap b/src/plugins/github/__snapshots__/graphView.test.js.snap deleted file mode 100644 index 634cb95..0000000 --- a/src/plugins/github/__snapshots__/graphView.test.js.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`plugins/github/graphView issues /#2 /comment #1 matches snapshot 1`] = ` -Object { - "id": "373768703", - "parent": Object { - "number": "2", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "ISSUE", - }, - "type": "COMMENT", -} -`; - -exports[`plugins/github/graphView issues /#2 matches snapshot 1`] = ` -Object { - "number": "2", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "ISSUE", -} -`; - -exports[`plugins/github/graphView issues /#2 number of comments matches snapshot 1`] = `8`; - -exports[`plugins/github/graphView issues number of issues matches snapshot 1`] = `10`; - -exports[`plugins/github/graphView pulls /#5 /comment #1 matches snapshot 1`] = ` -Object { - "id": "396430464", - "parent": Object { - "number": "5", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "PULL", - }, - "type": "COMMENT", -} -`; - -exports[`plugins/github/graphView pulls /#5 /review #1 /comment #1 matches snapshot 1`] = ` -Object { - "id": "171460198", - "parent": Object { - "id": "100313899", - "pull": Object { - "number": "5", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "PULL", - }, - "type": "REVIEW", - }, - "type": "COMMENT", -} -`; - -exports[`plugins/github/graphView pulls /#5 /review #1 has the right number of review comments 1`] = `1`; - -exports[`plugins/github/graphView pulls /#5 /review #1 matches snapshot 1`] = ` -Object { - "id": "100313899", - "pull": Object { - "number": "5", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "PULL", - }, - "type": "REVIEW", -} -`; - -exports[`plugins/github/graphView pulls /#5 matches snapshot 1`] = ` -Object { - "number": "5", - "repo": Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", - }, - "type": "PULL", -} -`; - -exports[`plugins/github/graphView pulls /#5 number of comments matches snapshot 1`] = `1`; - -exports[`plugins/github/graphView pulls /#5 number of reviews matches snapshot 1`] = `2`; - -exports[`plugins/github/graphView pulls number of pulls matches snapshot 1`] = `3`; - -exports[`plugins/github/graphView repo matches snapshot 1`] = ` -Object { - "name": "example-github", - "owner": "sourcecred", - "type": "REPO", -} -`; diff --git a/src/plugins/github/createGraph.test.js b/src/plugins/github/createGraph.test.js index 6ee43b6..8828d27 100644 --- a/src/plugins/github/createGraph.test.js +++ b/src/plugins/github/createGraph.test.js @@ -1,29 +1,9 @@ // @flow -import {GraphView} from "./graphView"; import {exampleGraph} from "./example/example"; describe("plugins/github/createGraph", () => { it("example graph matches snapshot", () => { expect(exampleGraph()).toMatchSnapshot(); }); - - it("passes all GraphView invariants", () => { - const view = new GraphView(exampleGraph()); - // This test is high leverage. It checks: - // - that every node starting with a GitHub prefix - // - can be structured using fromRaw - // - has the correct type - // - that every edge starting with a GitHub prefix - // - can be structured using fromRaw - // - and has the correct type, - // - and that its src has an expected prefix, - // - and that its dst has an expected prefix, - // - that every child node - // - has exactly one parent - // - has a parent with the correct type - view.checkInvariants(); - // as currently written, GV checks invariants on construction. - // we call the method explicitly as a defensive step. - }); }); diff --git a/src/plugins/github/graphView.js b/src/plugins/github/graphView.js deleted file mode 100644 index 3ad9412..0000000 --- a/src/plugins/github/graphView.js +++ /dev/null @@ -1,334 +0,0 @@ -// @flow - -import stringify from "json-stable-stringify"; -import deepEqual from "lodash.isequal"; - -import * as GN from "./nodes"; -import * as GE from "./edges"; - -import * as GitNode from "../git/nodes"; -import {ReactionContent$Values as Reactions} from "./graphqlTypes"; - -import { - Graph, - type NodeAddressT, - Direction, - type NeighborsOptions, - NodeAddress, - edgeToString, -} from "../../core/graph"; - -export class GraphView { - _graph: Graph; - _isCheckingInvariants: boolean; - - constructor(graph: Graph) { - this._graph = graph; - this._isCheckingInvariants = false; - this._maybeCheckInvariants(); - } - - graph(): Graph { - this._maybeCheckInvariants(); - return this._graph; - } - - *_nodes(prefix: GN.RawAddress): Iterator { - for (const n of this._graph.nodes({prefix})) { - const structured = GN.fromRaw((n: any)); - this._maybeCheckInvariants(); - yield (structured: any); - } - this._maybeCheckInvariants(); - } - - *_neighbors( - node: GN.StructuredAddress, - options: NeighborsOptions - ): Iterator { - if (!NodeAddress.hasPrefix(options.nodePrefix, GN.Prefix.base)) { - throw new Error(`_neighbors must filter to GitHub nodes`); - } - const rawNode = GN.toRaw(node); - for (const neighbor of this._graph.neighbors(rawNode, options)) { - this._maybeCheckInvariants(); - yield (GN.fromRaw((neighbor.node: any)): any); - } - this._maybeCheckInvariants(); - } - - _children( - node: GN.StructuredAddress, - nodePrefix: GN.RawAddress - ): Iterator { - const options = { - nodePrefix, - edgePrefix: GE.Prefix.hasParent, - direction: Direction.IN, - }; - return this._neighbors(node, options); - } - - repos(): Iterator { - return this._nodes(GN.Prefix.repo); - } - - issues(repo: GN.RepoAddress): Iterator { - return this._children(repo, GN.Prefix.issue); - } - - pulls(repo: GN.RepoAddress): Iterator { - return this._children(repo, GN.Prefix.pull); - } - - comments(commentable: GN.CommentableAddress): Iterator { - return this._children(commentable, GN.Prefix.comment); - } - - reviews(pull: GN.PullAddress): Iterator { - return this._children(pull, GN.Prefix.review); - } - - // TODO(@wchrgin) figure out how to overload this fn signature - parent(child: GN.ChildAddress): GN.ParentAddress { - const options = { - direction: Direction.OUT, - edgePrefix: GE.Prefix.hasParent, - nodePrefix: GN.Prefix.base, - }; - const parents: GN.ParentAddress[] = Array.from( - this._neighbors(child, options) - ); - if (parents.length !== 1) { - throw new Error( - `Parent invariant violated for child: ${stringify(child)}` - ); - } - return parents[0]; - } - - authors(content: GN.AuthorableAddress): Iterator { - const options = { - direction: Direction.IN, - edgePrefix: GE.Prefix.authors, - nodePrefix: GN.Prefix.userlike, - }; - return this._neighbors(content, options); - } - - _maybeCheckInvariants() { - if (this._isCheckingInvariants) { - return; - } - 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() { - this._isCheckingInvariants = true; - try { - this._checkInvariants(); - } finally { - this._isCheckingInvariants = false; - } - } - - _checkInvariants() { - const nodeTypeToParentAccessor = { - [GN.REPO_TYPE]: null, - [GN.ISSUE_TYPE]: (x) => x.repo, - [GN.PULL_TYPE]: (x) => x.repo, - [GN.COMMENT_TYPE]: (x) => x.parent, - [GN.REVIEW_TYPE]: (x) => x.pull, - [GN.USERLIKE_TYPE]: null, - [GitNode.COMMIT_TYPE]: null, - }; - for (const node of this._graph.nodes({prefix: GN.Prefix.base})) { - const structuredNode = GN.fromRaw((node: any)); - const type = structuredNode.type; - const parentAccessor = nodeTypeToParentAccessor[type]; - if (parentAccessor != null) { - // this.parent will throw error if there is not exactly 1 parent - const parent = this.parent((structuredNode: any)); - const expectedParent = parentAccessor((structuredNode: any)); - if (!deepEqual(parent, expectedParent)) { - throw new Error(`${stringify(structuredNode)} has the wrong parent`); - } - } - } - - type Hom = {| - +srcPrefix: NodeAddressT, - +dstPrefix: NodeAddressT, - |}; - function homProduct( - srcPrefixes: NodeAddressT[], - dstPrefixes: NodeAddressT[] - ): Hom[] { - const result = []; - for (const srcPrefix of srcPrefixes) { - for (const dstPrefix of dstPrefixes) { - result.push({srcPrefix, dstPrefix}); - } - } - return result; - } - type EdgeInvariant = {| - +homs: Hom[], - +srcAccessor?: (GE.StructuredAddress) => NodeAddressT, - +dstAccessor?: (GE.StructuredAddress) => NodeAddressT, - |}; - const edgeTypeToInvariants: {[type: string]: EdgeInvariant} = { - [GE.HAS_PARENT_TYPE]: { - homs: [ - {srcPrefix: GN.Prefix.issue, dstPrefix: GN.Prefix.repo}, - {srcPrefix: GN.Prefix.pull, dstPrefix: GN.Prefix.repo}, - {srcPrefix: GN.Prefix.review, dstPrefix: GN.Prefix.pull}, - {srcPrefix: GN.Prefix.reviewComment, dstPrefix: GN.Prefix.review}, - {srcPrefix: GN.Prefix.issueComment, dstPrefix: GN.Prefix.issue}, - {srcPrefix: GN.Prefix.pullComment, dstPrefix: GN.Prefix.pull}, - ], - srcAccessor: (x) => GN.toRaw((x: any).child), - }, - [GE.MERGED_AS_TYPE]: { - homs: [ - { - srcPrefix: GN.Prefix.pull, - dstPrefix: GitNode.Prefix.commit, - }, - ], - srcAccessor: (x) => GN.toRaw((x: any).pull), - }, - [GE.REFERENCES_TYPE]: { - homs: homProduct( - [ - GN.Prefix.issue, - GN.Prefix.pull, - GN.Prefix.review, - GN.Prefix.comment, - GitNode.Prefix.commit, - ], - [ - GN.Prefix.repo, - GN.Prefix.issue, - GN.Prefix.pull, - GN.Prefix.review, - GN.Prefix.comment, - GN.Prefix.userlike, - GitNode.Prefix.commit, - ] - ), - srcAccessor: (x) => GN.toRaw((x: any).referrer), - dstAccessor: (x) => GN.toRaw((x: any).referent), - }, - [GE.AUTHORS_TYPE]: { - homs: homProduct( - [GN.Prefix.userlike], - [ - GN.Prefix.issue, - GN.Prefix.review, - GN.Prefix.pull, - GN.Prefix.comment, - GitNode.Prefix.commit, - ] - ), - srcAccessor: (x) => GN.toRaw((x: any).author), - dstAccessor: (x) => GN.toRaw((x: any).content), - }, - [GE.MENTIONS_AUTHOR_TYPE]: { - homs: homProduct( - [GN.Prefix.issue, GN.Prefix.pull, GN.Prefix.comment], - [GN.Prefix.issue, GN.Prefix.pull, GN.Prefix.comment] - ), - srcAccessor: (x) => GN.toRaw((x: any).reference.src), - dstAccessor: (x) => GN.toRaw((x: any).reference.dst), - }, - [GE.REACTS_TYPE]: { - homs: homProduct( - [GN.Prefix.userlike], - [GN.Prefix.issue, GN.Prefix.pull, GN.Prefix.comment] - ), - srcAccessor: (x) => GN.toRaw((x: any).user), - dstAccessor: (x) => GN.toRaw((x: any).reactable), - }, - }; - - for (const edge of this._graph.edges({ - addressPrefix: GE.Prefix.base, - srcPrefix: NodeAddress.empty, - dstPrefix: NodeAddress.empty, - })) { - const address: GE.RawAddress = (edge.address: any); - const structuredEdge = GE.fromRaw(address); - const invariants = edgeTypeToInvariants[structuredEdge.type]; - if (invariants == null) { - throw new Error( - `Invariant: Unexpected edge type ${structuredEdge.type}` - ); - } - const {homs, srcAccessor, dstAccessor} = invariants; - if (srcAccessor) { - if (srcAccessor(structuredEdge) !== edge.src) { - throw new Error( - `Invariant: Expected src on edge ${edgeToString( - edge - )} to be ${srcAccessor(structuredEdge)}` - ); - } - } - if (dstAccessor) { - if (dstAccessor(structuredEdge) !== edge.dst) { - throw new Error( - `Invariant: Expected dst on edge ${edgeToString( - edge - )} to be ${dstAccessor(structuredEdge)}` - ); - } - } - let foundHom = false; - for (const {srcPrefix, dstPrefix} of homs) { - if ( - NodeAddress.hasPrefix(edge.src, srcPrefix) && - NodeAddress.hasPrefix(edge.dst, dstPrefix) - ) { - foundHom = true; - break; - } - } - if (!foundHom) { - throw new Error( - `Invariant: Edge ${stringify( - structuredEdge - )} with edge ${edgeToString( - edge - )} did not satisfy src/dst prefix requirements` - ); - } - } - - for (const reactionEdge of this._graph.edges({ - addressPrefix: GE.Prefix.reacts, - srcPrefix: NodeAddress.empty, - dstPrefix: NodeAddress.empty, - })) { - const address: GE.RawAddress = (reactionEdge.address: any); - const reactsAddress: GE.ReactsAddress = (GE.fromRaw(address): any); - const {reactionType} = reactsAddress; - if ( - reactionType !== Reactions.THUMBS_UP && - reactionType !== Reactions.HEART && - reactionType !== Reactions.HOORAY && - reactionType !== Reactions.ROCKET - ) { - throw new Error( - `Invariant: Edge ${stringify( - reactsAddress - )} has unspported reactionType` - ); - } - } - } -} diff --git a/src/plugins/github/graphView.test.js b/src/plugins/github/graphView.test.js deleted file mode 100644 index 455f859..0000000 --- a/src/plugins/github/graphView.test.js +++ /dev/null @@ -1,387 +0,0 @@ -// @flow - -import {Graph, type Edge, EdgeAddress} from "../../core/graph"; -import {GraphView} from "./graphView"; -import * as GE from "./edges"; -import * as GN from "./nodes"; -import {COMMIT_TYPE, toRaw as gitToRaw} from "../git/nodes"; -import {exampleGraph} from "./example/example"; - -function exampleView() { - return new GraphView(exampleGraph()); -} - -const decentralion: GN.UserlikeAddress = { - type: "USERLIKE", - subtype: "USER", - login: "decentralion", -}; -const wchargin: GN.UserlikeAddress = { - type: "USERLIKE", - subtype: "USER", - login: "wchargin", -}; - -describe("plugins/github/graphView", () => { - const view = exampleView(); - const repos = Array.from(view.repos()); - it("has one repo", () => { - expect(repos).toHaveLength(1); - }); - const repo = repos[0]; - it("repo matches snapshot", () => { - expect(repo).toMatchSnapshot(); - }); - - describe("issues", () => { - const issues = Array.from(view.issues(repo)); - it("number of issues matches snapshot", () => { - expect(issues.length).toMatchSnapshot(); - }); - - describe("/#2", () => { - const issue = issues[1]; - it("matches snapshot", () => { - expect(issue).toMatchSnapshot(); - }); - it("is issue #2", () => { - expect(issue.number).toBe("2"); - }); - it("is authored by decentralion", () => { - expect(Array.from(view.authors(issue))).toEqual([decentralion]); - }); - it("has the right parent", () => { - expect(view.parent(issue)).toEqual(repo); - }); - const comments = Array.from(view.comments(issue)); - it("number of comments matches snapshot", () => { - expect(comments.length).toMatchSnapshot(); - }); - - describe("/comment #1", () => { - const comment = comments[0]; - it("matches snapshot", () => { - expect(comment).toMatchSnapshot(); - }); - it("has the right parent", () => { - expect(view.parent(comment)).toEqual(issue); - }); - it("is authored by decentralion", () => { - expect(Array.from(view.authors(comment))).toEqual([decentralion]); - }); - }); - }); - }); - - describe("pulls", () => { - const pulls = Array.from(view.pulls(repo)); - it("number of pulls matches snapshot", () => { - expect(pulls.length).toMatchSnapshot(); - }); - - describe("/#5", () => { - const pull = pulls[1]; - it("matches snapshot", () => { - expect(pull).toMatchSnapshot(); - }); - it("is pull #5", () => { - expect(pull.number).toBe("5"); - }); - it("is authored by decentralion", () => { - expect(Array.from(view.authors(pull))).toEqual([decentralion]); - }); - it("has the right parent", () => { - expect(view.parent(pull)).toEqual(repo); - }); - const comments = Array.from(view.comments(pull)); - it("number of comments matches snapshot", () => { - expect(comments.length).toMatchSnapshot(); - }); - - describe("/comment #1", () => { - const comment = comments[0]; - it("matches snapshot", () => { - expect(comment).toMatchSnapshot(); - }); - it("has the right parent", () => { - expect(view.parent(comment)).toEqual(pull); - }); - it("is authored by wchargin", () => { - expect(Array.from(view.authors(comment))).toEqual([wchargin]); - }); - }); - const reviews = Array.from(view.reviews(pull)); - it("number of reviews matches snapshot", () => { - expect(reviews.length).toMatchSnapshot(); - }); - - describe("/review #1", () => { - const review = reviews[0]; - it("matches snapshot", () => { - expect(review).toMatchSnapshot(); - }); - it("has the right parent", () => { - expect(view.parent(review)).toEqual(pull); - }); - it("is authored by wchargin", () => { - expect(Array.from(view.authors(review))).toEqual([wchargin]); - }); - const reviewComments = Array.from(view.comments(review)); - it("has the right number of review comments", () => { - expect(reviewComments.length).toMatchSnapshot(); - }); - - describe("/comment #1", () => { - const reviewComment = reviewComments[0]; - it("is authored by wchargin", () => { - expect(Array.from(view.authors(reviewComment))).toEqual([wchargin]); - }); - it("matches snapshot", () => { - expect(reviewComment).toMatchSnapshot(); - }); - it("has the right parent", () => { - expect(view.parent(reviewComment)).toEqual(review); - }); - }); - }); - }); - }); - describe("invariants", () => { - const userlike: GN.UserlikeAddress = { - type: "USERLIKE", - subtype: "USER", - login: "decentralion", - }; - const repo: GN.RepoAddress = { - type: "REPO", - owner: "sourcecred", - name: "example-github", - }; - const issue: GN.IssueAddress = {type: "ISSUE", repo, number: "11"}; - const pull: GN.PullAddress = {type: "PULL", repo, number: "12"}; - const review: GN.ReviewAddress = {type: "REVIEW", pull, id: "foo"}; - const issueComment: GN.CommentAddress = { - type: "COMMENT", - parent: issue, - id: "bar", - }; - const pullComment: GN.CommentAddress = { - type: "COMMENT", - parent: pull, - id: "bar1", - }; - const reviewComment: GN.CommentAddress = { - type: "COMMENT", - parent: review, - id: "bar2", - }; - const nameToAddress = { - userlike: userlike, - repo: repo, - issue: issue, - pull: pull, - review: review, - issueComment: issueComment, - pullComment: pullComment, - reviewComment: reviewComment, - }; - describe("there must be parents for", () => { - function needParentFor(name: string) { - it(name, () => { - const g = new Graph(); - const example = nameToAddress[name]; - g.addNode(GN.toRaw(example)); - expect(() => new GraphView(g)).toThrow("Parent invariant"); - }); - } - needParentFor("issue"); - needParentFor("pull"); - needParentFor("review"); - needParentFor("review"); - needParentFor("issueComment"); - needParentFor("pullComment"); - needParentFor("reviewComment"); - }); - - describe("edge invariants", () => { - const exampleWithParents = () => { - const g = new Graph() - .addNode(GN.toRaw(repo)) - .addNode(GN.toRaw(issue)) - .addEdge(GE.createEdge.hasParent(issue, repo)) - .addNode(GN.toRaw(pull)) - .addEdge(GE.createEdge.hasParent(pull, repo)) - .addNode(GN.toRaw(review)) - .addEdge(GE.createEdge.hasParent(review, pull)) - .addNode(GN.toRaw(issueComment)) - .addEdge(GE.createEdge.hasParent(issueComment, issue)) - .addNode(GN.toRaw(pullComment)) - .addEdge(GE.createEdge.hasParent(pullComment, pull)) - .addNode(GN.toRaw(reviewComment)) - .addEdge(GE.createEdge.hasParent(reviewComment, review)) - .addNode(GN.toRaw(userlike)); - return g; - }; - function failsForEdge(edge: Edge) { - const g = exampleWithParents() - .addNode(edge.src) - .addNode(edge.dst) - .addEdge(edge); - expect(() => new GraphView(g)).toThrow("Invariant: Edge"); - } - describe("authors edges", () => { - it("src must be userlike", () => { - // $ExpectFlowError - const badEdge = GE.createEdge.authors(pull, issue); - failsForEdge(badEdge); - }); - it("dst must be authorable", () => { - // $ExpectFlowError - const badEdge = GE.createEdge.authors(userlike, repo); - failsForEdge(badEdge); - }); - it("src must be author in edge address", () => { - const otherAuthor = { - type: "USERLIKE", - subtype: "USER", - login: "wchargin", - }; - const authorsEdge = GE.createEdge.authors(otherAuthor, issue); - (authorsEdge: any).src = GN.toRaw(userlike); - const g = exampleWithParents().addEdge(authorsEdge); - expect(() => new GraphView(g)).toThrow("Invariant: Expected src"); - }); - it("dst must be content in edge address", () => { - const authorsEdge = GE.createEdge.authors(userlike, issue); - (authorsEdge: any).dst = GN.toRaw(pull); - const g = exampleWithParents().addEdge(authorsEdge); - expect(() => new GraphView(g)).toThrow("Invariant: Expected dst"); - }); - }); - describe("merged as edges", () => { - const commit = {type: COMMIT_TYPE, hash: "hash"}; - it("src must be a pull", () => { - // $ExpectFlowError - const badEdge = GE.createEdge.mergedAs(issue, commit); - failsForEdge(badEdge); - }); - it("src must be pull in edge address", () => { - const otherPull = {type: "PULL", repo, number: "143"}; - const mergedAs = GE.createEdge.mergedAs(otherPull, commit); - (mergedAs: any).src = GN.toRaw(pull); - const g = exampleWithParents() - .addNode(gitToRaw(commit)) - .addEdge(mergedAs); - expect(() => new GraphView(g)).toThrow("Invariant: Expected src"); - }); - }); - describe("references edges", () => { - it("src must be a TextContentAddress", () => { - // $ExpectFlowError - const badEdge = GE.createEdge.references(userlike, pull); - failsForEdge(badEdge); - }); - it("src must be the referrer in edge address", () => { - const references = GE.createEdge.references(issue, review); - (references: any).src = GN.toRaw(pull); - const g = exampleWithParents().addEdge(references); - expect(() => new GraphView(g)).toThrow("Invariant: Expected src"); - }); - it("dst must be referent in edge address", () => { - const references = GE.createEdge.references(issue, review); - (references: any).dst = GN.toRaw(pull); - const g = exampleWithParents().addEdge(references); - expect(() => new GraphView(g)).toThrow("Invariant: Expected dst"); - }); - }); - describe("has parent edges", () => { - it("must satisfy specific hom relationships", () => { - const g = new Graph() - .addNode(GN.toRaw(repo)) - // $ExpectFlowError - .addEdge(GE.createEdge.hasParent(repo, repo)); - expect(() => new GraphView(g)).toThrow("Invariant: Edge"); - }); - it("must be unique", () => { - const otherRepo = {type: "REPO", owner: "foo", name: "bar"}; - const g = exampleWithParents(); - g.addNode(GN.toRaw(otherRepo)); - const otherParent = { - src: GN.toRaw(issue), - dst: GN.toRaw(otherRepo), - address: EdgeAddress.append(GE.Prefix.hasParent, "foobar"), - }; - g.addEdge(otherParent); - expect(() => new GraphView(g)).toThrow("Parent invariant"); - }); - it("must match the parent specified in the node address", () => { - const otherRepo = {type: "REPO", owner: "foo", name: "bar"}; - const otherParent = GE.createEdge.hasParent(issue, otherRepo); - const g = new Graph() - .addNode(GN.toRaw(issue)) - .addNode(GN.toRaw(otherRepo)) - .addEdge(otherParent); - expect(() => new GraphView(g)).toThrow("has the wrong parent"); - }); - it("must match child specified in the edge address", () => { - const parent = GE.createEdge.hasParent(issue, repo); - (parent: any).src = GN.toRaw(pull); - const g = new Graph() - .addNode(GN.toRaw(pull)) - .addNode(GN.toRaw(repo)) - .addEdge(parent); - expect(() => new GraphView(g)).toThrow("Invariant: Expected src"); - }); - }); - describe("reactions edges", () => { - it("must have a supported type", () => { - const unsupported = ["THUMBS_DOWN", "LAUGH", "CONFUSED"]; - for (const u of unsupported) { - failsForEdge(GE.createEdge.reacts(u, userlike, issue)); - } - }); - }); - }); - - it("are properly re-entrant", () => { - const g = new Graph(); - const view = new GraphView(g); - // no error, empty graph is fine - view.graph(); - // introduce an invariant violation (no parent) - g.addNode(GN.toRaw(issue)); - try { - view.graph(); - } catch (_) {} - expect(() => view.graph()).toThrow("invariant violated"); - }); - - describe("are checked on every public method", () => { - const badView = () => { - const g = new Graph(); - const view = new GraphView(g); - g.addNode(GN.toRaw(issue)); - g.addNode(GN.toRaw(pull)); - g.addNode(GN.toRaw(repo)); - return view; - }; - const methods = { - graph: () => badView().graph(), - repos: () => Array.from(badView().repos()), - issues: () => Array.from(badView().issues(repo)), - pulls: () => Array.from(badView().pulls(repo)), - comments: () => Array.from(badView().comments(issue)), - reviews: () => Array.from(badView().reviews(pull)), - parent: () => badView().parent(pull), - authors: () => Array.from(badView().authors(pull)), - }; - - for (const name of Object.keys(methods)) { - it(`including ${name}`, () => { - const method = methods[name]; - expect(() => method()).toThrow("invariant"); - }); - } - }); - }); -});