Remove the `graphView`s (#1171)

A long time ago, we made graph views for git and github. These are
interfaces over the graph which allow retrieving nodes' relations, e.g.
finding the parent address of a commit just using the graph.

These are fairly complex, and have seen almost no use at all. The one
thing they are used for is implementing invariant checking. The
invariant checking is nice in principle, but since we only apply it to
the example data, its of very limited value in practice.

Since I'm planning a significant Graph refactor (#1136), I'd rather delete this
code than continue to maintain it, since I think it's complexity/value
ratio is unfavorable.

Test plan: `yarn test`
This commit is contained in:
Dandelion Mané 2019-06-03 21:07:27 +03:00 committed by GitHub
parent fcbd024a83
commit 16edea6413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 0 additions and 1126 deletions

View File

@ -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()) {

View File

@ -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<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();
}
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;
}
_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)}`
);
}
}
}
}

View File

@ -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<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("#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();
});
});
});
});
});

View File

@ -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",
}
`;

View File

@ -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.
});
});

View File

@ -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<T: GN.StructuredAddress>(prefix: GN.RawAddress): Iterator<T> {
for (const n of this._graph.nodes({prefix})) {
const structured = GN.fromRaw((n: any));
this._maybeCheckInvariants();
yield (structured: any);
}
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 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<T: GN.StructuredAddress>(
node: GN.StructuredAddress,
nodePrefix: GN.RawAddress
): Iterator<T> {
const options = {
nodePrefix,
edgePrefix: GE.Prefix.hasParent,
direction: Direction.IN,
};
return this._neighbors(node, options);
}
repos(): Iterator<GN.RepoAddress> {
return this._nodes(GN.Prefix.repo);
}
issues(repo: GN.RepoAddress): Iterator<GN.IssueAddress> {
return this._children(repo, GN.Prefix.issue);
}
pulls(repo: GN.RepoAddress): Iterator<GN.PullAddress> {
return this._children(repo, GN.Prefix.pull);
}
comments(commentable: GN.CommentableAddress): Iterator<GN.CommentAddress> {
return this._children(commentable, GN.Prefix.comment);
}
reviews(pull: GN.PullAddress): Iterator<GN.ReviewAddress> {
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<GN.UserlikeAddress> {
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`
);
}
}
}
}

View File

@ -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");
});
}
});
});
});