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:
parent
fcbd024a83
commit
16edea6413
|
@ -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()) {
|
||||
|
|
|
@ -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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
}
|
||||
`;
|
|
@ -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.
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue