From a8c51bcdfb2a8133bce85fad179753d5a3f270d5 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Fri, 11 Dec 2020 18:16:16 -0500 Subject: [PATCH 1/3] Reworking graph data structure to use Map Fixes #38 --- src/extension.ts | 47 ++++--------- src/parsing.ts | 26 ++++---- src/types.ts | 170 +++++++++++++++++++++++++++++++++++++++++++++-- src/utils.ts | 26 +++----- 4 files changed, 197 insertions(+), 72 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index d1a9801..875104b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,6 @@ import { TextDecoder } from "util"; import * as path from "path"; import { parseFile, parseDirectory, learnFileId } from "./parsing"; import { - filterNonExistingEdges, getColumnSetting, getConfiguration, getFileTypesSetting, @@ -32,37 +31,33 @@ const watch = ( const sendGraph = () => { panel.webview.postMessage({ type: "refresh", - payload: graph, + payload: graph.toD3Graph(), }); }; // Watch file changes in case user adds a link. watcher.onDidChange(async (event) => { await parseFile(graph, event.path); - filterNonExistingEdges(graph); + graph.fixEdges(); sendGraph(); }); // Watch file creation in case user adds a new file. watcher.onDidCreate(async (event) => { await parseFile(graph, event.path); - filterNonExistingEdges(graph); + graph.fixEdges(); sendGraph(); }); + // Watch file deletion and remove the matching node from the graph. watcher.onDidDelete(async (event) => { const filePath = path.normalize(event.path); - const index = graph.nodes.findIndex((node) => node.path === filePath); - if (index === -1) { + const node = graph.getNodeByPath(filePath); + if (!node) { return; } - graph.nodes.splice(index, 1); - graph.edges = graph.edges.filter( - (edge) => edge.source !== filePath && edge.target !== filePath - ); - - filterNonExistingEdges(graph); + graph.removeNode(node.id); sendGraph(); }); @@ -80,25 +75,10 @@ const watch = ( for (const file of event.files) { const previous = path.normalize(file.oldUri.path); const next = path.normalize(file.newUri.path); - - for (const edge of graph.edges) { - if (edge.source === previous) { - edge.source = next; - } - - if (edge.target === previous) { - edge.target = next; - } - } - - for (const node of graph.nodes) { - if (node.path === previous) { - node.path = next; - } - } - - sendGraph(); + graph.updateNodePath(previous, next); } + + sendGraph(); }); panel.webview.onDidReceiveMessage( @@ -146,14 +126,11 @@ export function activate(context: vscode.ExtensionContext) { return; } - const graph: Graph = { - nodes: [], - edges: [], - }; + const graph: Graph = new Graph(); await parseDirectory(graph, learnFileId); await parseDirectory(graph, parseFile); - filterNonExistingEdges(graph); + graph.fixEdges(); panel.webview.html = await getWebviewContent(context, panel); diff --git a/src/parsing.ts b/src/parsing.ts index 262f33d..f4b0579 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -4,7 +4,7 @@ import * as unified from "unified"; import * as markdown from "remark-parse"; import * as wikiLinkPlugin from "remark-wiki-link"; import * as frontmatter from "remark-frontmatter"; -import { MarkdownNode, Graph } from "./types"; +import { MarkdownNode, Graph, Node } from "./types"; import { TextDecoder } from "util"; import { findTitle, @@ -41,27 +41,25 @@ export const parseFile = async (graph: Graph, filePath: string) => { let title: string | null = findTitle(ast); - const index = graph.nodes.findIndex((node) => node.path === filePath); - + const node = graph.getNodeByPath(filePath); if (!title) { - if (index !== -1) { - graph.nodes.splice(index, 1); + if (node) { + graph.removeNode(node.id); } - return; } - if (index !== -1) { - graph.nodes[index].label = title; + const nodeId = node ? node.id : id(filePath); + if (node) { + node.label = title; + // Remove edges based on an old version of this file. + graph.clearNodeLinks(nodeId); } else { - graph.nodes.push({ id: id(filePath), path: filePath, label: title }); + graph.addNode(new Node(nodeId, filePath, title)); } - // Remove edges based on an old version of this file. - graph.edges = graph.edges.filter((edge) => edge.source !== id(filePath)); - // Returns a list of decoded links (by default markdown only supports encoded URI) - const links = findLinks(ast).map(uri => decodeURI(uri)); + const links = findLinks(ast).map((uri) => decodeURI(uri)); const parentDirectory = filePath.split(path.sep).slice(0, -1).join(path.sep); for (const link of links) { @@ -70,7 +68,7 @@ export const parseFile = async (graph: Graph, filePath: string) => { target = path.normalize(`${parentDirectory}/${link}`); } - graph.edges.push({ source: id(filePath), target: id(target) }); + graph.addLink(nodeId, id(target)); } }; diff --git a/src/types.ts b/src/types.ts index 6da48df..de1d4b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,17 +1,173 @@ -export type Edge = { - source: string; - target: string; +import { StringifyOptions } from "querystring"; +import { runInThisContext } from "vm"; + +export class Node { + public id: string; + public path: string; + public label: string; + public links: Set = new Set(); + public backlinks: Set = new Set(); + + constructor(id: string, path: string, label: string) { + this.id = id; + this.path = path; + this.label = label; + } + + public addLink(targetNodeId: string): void { + this.links.add(targetNodeId); + } + + public addBacklink(srcNodeId: string): void { + this.backlinks.add(srcNodeId); + } + + public removeLink(dstNodeId: string): void { + this.links.delete(dstNodeId); + } + + public removeBacklink(srcNodeId: string): void { + this.backlinks.delete(srcNodeId); + } + + public filterLinks(filterFunc: (dstNodeId: string) => boolean): void { + this.links = new Set([...this.links.values()].filter(filterFunc)); + } + + public filterBacklinks(filterFunc: (srcNodeId: string) => boolean): void { + this.backlinks = new Set([...this.backlinks.values()].filter(filterFunc)); + } + + public toD3Node(): D3Node { + return { + id: this.id, + path: this.path, + label: this.label, + }; + } + + public toD3Edges(): D3Edge[] { + const edges: D3Edge[] = []; + this.links.forEach((dstNodeId) => edges.push({ + source: this.id, + target: dstNodeId, + })); + return edges; + } +} + +export class Graph { + private nodes: Map; + + constructor() { + this.nodes = new Map(); + } + + public getNode(id: string): Node | undefined { + return this.nodes.get(id); + } + + public getNodeByPath(path: string): Node | undefined { + for (const node of this.nodes.values()) { + if (node.path === path) { + return node; + } + } + return undefined; + } + + /** + * Removes a node from the graph, along with any links or backlinks pointing to it. + * @param id The id of the node to be removed. + */ + public removeNode(id: string): void { + this.nodes.get(id)?.links.forEach((dstNodeId) => this.nodes.get(dstNodeId)?.removeBacklink(id)); + this.nodes.get(id)?.backlinks.forEach((srcNodeId) => this.nodes.get(srcNodeId)?.removeLink(id)); + this.nodes.delete(id); + } + + public updateNodePath(oldPath: string, newPath: string): void { + const node = this.getNodeByPath(oldPath); + if (node) { + node.path = newPath; + } + } + + /** + * Removes any links/backlinks where the linked node does not exist and adds backlinks. + * You should run this method after adding one or more nodes or edges to the graph. + */ + public fixEdges(): void { + this.nodes.forEach((node) => { + node.filterLinks((dstNodeId) => this.nodes.has(dstNodeId)); + node.filterBacklinks((srcNodeId) => this.nodes.has(srcNodeId)); + node.links.forEach((dstNodeId) => this.nodes.get(dstNodeId)?.addBacklink(node.id)); + }); + } + + /** + * Adds a node to the graph. If the node includes links or backlinks, the graph may be inconsistent + * until {@link fixEdges} is called. + * @param node The node to be added to the graph. + */ + public addNode(node: Node): void { + this.nodes.set(node.id, node); + } + + /** + * Adds a link from srcNodeId to dstNodeId. It does not automatically add a backlink on the + * destination node, so the graph will be inconsistent until {@link fixEdges} is called. + * @param srcNodeId The id of the node that the link originates from + * @param dstNodeId The id of the node the link goes to. + */ + public addLink(srcNodeId: string, dstNodeId: string): void { + this.nodes.get(srcNodeId)?.addLink(dstNodeId); + } + + + /** + * Removes all links from the specified node, as well as any matching backlinks. + * @param nodeId the node to remove all links from. + */ + public clearNodeLinks(nodeId: string): void { + const node = this.nodes.get(nodeId); + if (!node) { + return; + } + node.links.forEach((dstNodeId) => this.nodes.get(dstNodeId)?.removeBacklink(nodeId)); + node.links = new Set(); + } + + /** + * Outputs a javascript object representing the graph that can be rendered in D3. + */ + public toD3Graph(): D3Graph { + const graph: D3Graph = { + nodes: [], + edges: [], + }; + this.nodes.forEach((node) => { + graph.nodes.push(node.toD3Node()); + graph.edges.push(...node.toD3Edges()); + }); + return graph; + } +} + +export type D3Graph = { + nodes: D3Node[]; + edges: D3Edge[]; }; -export type Node = { +export type D3Node = { id: string; path: string; label: string; }; -export type Graph = { - nodes: Node[]; - edges: Edge[]; +export type D3Edge = { + source: string; + target: string; }; export type MarkdownNode = { diff --git a/src/utils.ts b/src/utils.ts index 1f43f61..9dcbe0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,7 +46,7 @@ export const findTitle = (ast: MarkdownNode): string | null => { child.children && child.children.length > 0 ) { - let title = child.children[0].value! + let title = child.children[0].value!; const titleMaxLength = getTitleMaxLength(); if (titleMaxLength > 0 && title.length > titleMaxLength) { @@ -83,7 +83,7 @@ const settingToValue: { [key: string]: vscode.ViewColumn | undefined } = { export const getTitleMaxLength = () => { return getConfiguration("titleMaxLength"); -} +}; export const getColumnSetting = (key: string) => { const column = getConfiguration(key); @@ -107,18 +107,12 @@ export const getFileTypesSetting = () => { return getConfiguration("fileTypes") || DEFAULT_VALUE; }; -export const getDot = (graph: Graph) => `digraph g { - ${graph.nodes - .map((node) => ` ${node.id} [label="${node.label}"];`) - .join("\n")} - ${graph.edges.map((edge) => ` ${edge.source} -> ${edge.target}`).join("\n")} +export const getDot = (graph: Graph) => { + const d3Graph = graph.toD3Graph(); + return `digraph g { + ${d3Graph.nodes + .map((node) => ` ${node.id} [label="${node.label}"];`) + .join("\n")} + ${d3Graph.edges.map((edge) => ` ${edge.source} -> ${edge.target}`).join("\n")} }`; - -export const exists = (graph: Graph, id: string) => - !!graph.nodes.find((node) => node.id === id); - -export const filterNonExistingEdges = (graph: Graph) => { - graph.edges = graph.edges.filter( - (edge) => exists(graph, edge.source) && exists(graph, edge.target) - ); -}; +}; \ No newline at end of file From b97ff6868146348419ee2b44c8b71d56c448c328 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Fri, 11 Dec 2020 18:27:27 -0500 Subject: [PATCH 2/3] Fixing test running and adding tests for graph --- .yarnrc | 1 + package.json | 4 +- src/test/suite/extension.test.ts | 6 +- src/test/suite/graph.test.ts | 108 +++++++++++++++++++++++++++++++ src/test/suite/index.ts | 2 +- yarn.lock | 49 ++++++++++++++ 6 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 .yarnrc create mode 100644 src/test/suite/graph.test.ts diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..f757a6a --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +--ignore-engines true \ No newline at end of file diff --git a/package.json b/package.json index 516920b..3d7c534 100644 --- a/package.json +++ b/package.json @@ -81,16 +81,18 @@ "compile": "webpack --mode production", "lint": "eslint src --ext ts", "watch": "tsc -watch -p ./", - "pretest": "yarn run compile && yarn run lint", + "pretest": "yarn run test-compile && yarn run lint", "test": "node ./out/test/runTest.js" }, "devDependencies": { + "@types/chai": "^4.2.14", "@types/glob": "^7.1.1", "@types/mocha": "^7.0.2", "@types/node": "^13.11.0", "@types/vscode": "^1.45.0", "@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/parser": "^2.33.0", + "chai": "^4.2.0", "eslint": "^6.8.0", "glob": "^7.1.6", "mocha": "^7.1.2", diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 08a6f78..7ddd724 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,14 +1,14 @@ -import * as assert from 'assert'; +import { assert } from 'chai'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; // import * as myExtension from '../../extension'; -suite('Extension Test Suite', () => { +describe('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); - test('Sample test', () => { + context('Sample test', function() { assert.equal(-1, [1, 2, 3].indexOf(5)); assert.equal(-1, [1, 2, 3].indexOf(0)); }); diff --git a/src/test/suite/graph.test.ts b/src/test/suite/graph.test.ts new file mode 100644 index 0000000..f113aed --- /dev/null +++ b/src/test/suite/graph.test.ts @@ -0,0 +1,108 @@ +import { assert } from 'chai'; +import { Graph, Node } from '../../types'; + +describe('Graph', function() { + context('given an empty graph', function() { + let graph: Graph; + + beforeEach(() => { + graph = new Graph(); + }); + + context('when a node with extra links and backlinks is added', function() { + const firstNode = new Node('firstNodeId', '/tmp/nodeOne.md', 'Node One'); + firstNode.addLink('bogusLink'); + firstNode.addBacklink('bogusBacklink'); + let retrievedNode: Node | undefined; + beforeEach(() => { + graph.addNode(firstNode); + retrievedNode = graph.getNode(firstNode.id); + }); + + it('appears in the graph', function() { + assert.exists(retrievedNode); + }); + + it('does not remove the broken links', function() { + assert.equal(retrievedNode?.links.size, firstNode.links.size); + }); + + it('does not remove the broken backlinks', function() { + assert.equal(retrievedNode?.backlinks.size, firstNode.backlinks.size); + }); + + context("when fixEdges is called on the graph", function() { + let retrievedNode: Node | undefined; + beforeEach(() => { + graph.fixEdges(); + retrievedNode = graph.getNode(firstNode.id); + }); + + it('removes the broken links', function() { + assert.equal(retrievedNode?.links.size, 0); + }); + + it('removes the broken backlinks', function() { + assert.equal(retrievedNode?.backlinks.size, 0); + }); + }); + + context("when a second node is added", function() { + const secondNode = new Node('secondNodeId', '/tmp/nodeTwo.md', 'Node Two'); + beforeEach(() => graph.addNode(secondNode)); + + context("when a link is added from the first node to the second node", function() { + beforeEach(() => graph.addLink(firstNode.id, secondNode.id)); + + it("adds the link to the first node", function() { + const retrievedNode = graph.getNode(firstNode.id); + assert.exists(retrievedNode); + assert.include([...retrievedNode?.links.values()!], secondNode.id); + }); + + it("does not add the backlink to the second node", function() { + const retrievedNode = graph.getNode(secondNode.id); + assert.exists(retrievedNode); + assert.notInclude([...retrievedNode?.backlinks.values()!], secondNode.id); + }); + + context("when fixEdges is called on the graph", function() { + let retrievedFirstNode: Node; + let retrievedSecondNode: Node; + beforeEach(() => { + graph.fixEdges(); + retrievedFirstNode = graph.getNode(firstNode.id)!; + retrievedSecondNode = graph.getNode(secondNode.id)!; + }); + + it("does not remove the valid link", function() { + assert.include([...retrievedFirstNode!.links.values()], secondNode.id); + }); + + it("adds a backlink to the second node", function() { + assert.include([...retrievedSecondNode.backlinks.values()], firstNode.id); + }); + }); + + context("when clearNodeLinks is called for the first node", function() { + let retrievedFirstNode: Node; + let retrievedSecondNode: Node; + beforeEach(() => { + graph.clearNodeLinks(firstNode.id); + retrievedFirstNode = graph.getNode(firstNode.id)!; + retrievedSecondNode = graph.getNode(secondNode.id)!; + }); + + it("removes all links from the first node", function() { + assert.equal(retrievedFirstNode!.links.size, 0); + }); + + it("removes the backlink from the first node", function() { + assert.notInclude([...retrievedSecondNode!.backlinks.values()], firstNode.id); + }); + }); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 7029e38..596e95a 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -5,7 +5,7 @@ import * as glob from 'glob'; export function run(): Promise { // Create the mocha test const mocha = new Mocha({ - ui: 'tdd', + ui: 'bdd', color: true }); diff --git a/yarn.lock b/yarn.lock index 9955519..b3db9cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,11 @@ dependencies: regenerator-runtime "^0.13.4" +"@types/chai@^4.2.14": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" + integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -438,6 +443,11 @@ assert@^1.1.1: object-assign "^4.1.1" util "0.10.3" +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -700,6 +710,18 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== +chai@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -742,6 +764,11 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + chokidar@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" @@ -1048,6 +1075,13 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1635,6 +1669,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2847,6 +2886,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -3740,6 +3784,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" From 448514b8f95178919b3f9e5e70502a26208c0688 Mon Sep 17 00:00:00 2001 From: Joshua Matthews Date: Tue, 29 Dec 2020 21:49:19 -0500 Subject: [PATCH 3/3] Formatting graph tests with prettier --- src/test/suite/graph.test.ts | 241 +++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 108 deletions(-) diff --git a/src/test/suite/graph.test.ts b/src/test/suite/graph.test.ts index f113aed..38928e5 100644 --- a/src/test/suite/graph.test.ts +++ b/src/test/suite/graph.test.ts @@ -1,108 +1,133 @@ -import { assert } from 'chai'; -import { Graph, Node } from '../../types'; - -describe('Graph', function() { - context('given an empty graph', function() { - let graph: Graph; - - beforeEach(() => { - graph = new Graph(); - }); - - context('when a node with extra links and backlinks is added', function() { - const firstNode = new Node('firstNodeId', '/tmp/nodeOne.md', 'Node One'); - firstNode.addLink('bogusLink'); - firstNode.addBacklink('bogusBacklink'); - let retrievedNode: Node | undefined; - beforeEach(() => { - graph.addNode(firstNode); - retrievedNode = graph.getNode(firstNode.id); - }); - - it('appears in the graph', function() { - assert.exists(retrievedNode); - }); - - it('does not remove the broken links', function() { - assert.equal(retrievedNode?.links.size, firstNode.links.size); - }); - - it('does not remove the broken backlinks', function() { - assert.equal(retrievedNode?.backlinks.size, firstNode.backlinks.size); - }); - - context("when fixEdges is called on the graph", function() { - let retrievedNode: Node | undefined; - beforeEach(() => { - graph.fixEdges(); - retrievedNode = graph.getNode(firstNode.id); - }); - - it('removes the broken links', function() { - assert.equal(retrievedNode?.links.size, 0); - }); - - it('removes the broken backlinks', function() { - assert.equal(retrievedNode?.backlinks.size, 0); - }); - }); - - context("when a second node is added", function() { - const secondNode = new Node('secondNodeId', '/tmp/nodeTwo.md', 'Node Two'); - beforeEach(() => graph.addNode(secondNode)); - - context("when a link is added from the first node to the second node", function() { - beforeEach(() => graph.addLink(firstNode.id, secondNode.id)); - - it("adds the link to the first node", function() { - const retrievedNode = graph.getNode(firstNode.id); - assert.exists(retrievedNode); - assert.include([...retrievedNode?.links.values()!], secondNode.id); - }); - - it("does not add the backlink to the second node", function() { - const retrievedNode = graph.getNode(secondNode.id); - assert.exists(retrievedNode); - assert.notInclude([...retrievedNode?.backlinks.values()!], secondNode.id); - }); - - context("when fixEdges is called on the graph", function() { - let retrievedFirstNode: Node; - let retrievedSecondNode: Node; - beforeEach(() => { - graph.fixEdges(); - retrievedFirstNode = graph.getNode(firstNode.id)!; - retrievedSecondNode = graph.getNode(secondNode.id)!; - }); - - it("does not remove the valid link", function() { - assert.include([...retrievedFirstNode!.links.values()], secondNode.id); - }); - - it("adds a backlink to the second node", function() { - assert.include([...retrievedSecondNode.backlinks.values()], firstNode.id); - }); - }); - - context("when clearNodeLinks is called for the first node", function() { - let retrievedFirstNode: Node; - let retrievedSecondNode: Node; - beforeEach(() => { - graph.clearNodeLinks(firstNode.id); - retrievedFirstNode = graph.getNode(firstNode.id)!; - retrievedSecondNode = graph.getNode(secondNode.id)!; - }); - - it("removes all links from the first node", function() { - assert.equal(retrievedFirstNode!.links.size, 0); - }); - - it("removes the backlink from the first node", function() { - assert.notInclude([...retrievedSecondNode!.backlinks.values()], firstNode.id); - }); - }); - }); - }); - }); - }); -}); \ No newline at end of file +import { assert } from "chai"; +import { Graph, Node } from "../../types"; + +describe("Graph", function () { + context("given an empty graph", function () { + let graph: Graph; + + beforeEach(() => { + graph = new Graph(); + }); + + context("when a node with extra links and backlinks is added", function () { + const firstNode = new Node("firstNodeId", "/tmp/nodeOne.md", "Node One"); + firstNode.addLink("bogusLink"); + firstNode.addBacklink("bogusBacklink"); + let retrievedNode: Node | undefined; + beforeEach(() => { + graph.addNode(firstNode); + retrievedNode = graph.getNode(firstNode.id); + }); + + it("appears in the graph", function () { + assert.exists(retrievedNode); + }); + + it("does not remove the broken links", function () { + assert.equal(retrievedNode?.links.size, firstNode.links.size); + }); + + it("does not remove the broken backlinks", function () { + assert.equal(retrievedNode?.backlinks.size, firstNode.backlinks.size); + }); + + context("when fixEdges is called on the graph", function () { + let retrievedNode: Node | undefined; + beforeEach(() => { + graph.fixEdges(); + retrievedNode = graph.getNode(firstNode.id); + }); + + it("removes the broken links", function () { + assert.equal(retrievedNode?.links.size, 0); + }); + + it("removes the broken backlinks", function () { + assert.equal(retrievedNode?.backlinks.size, 0); + }); + }); + + context("when a second node is added", function () { + const secondNode = new Node( + "secondNodeId", + "/tmp/nodeTwo.md", + "Node Two" + ); + beforeEach(() => graph.addNode(secondNode)); + + context( + "when a link is added from the first node to the second node", + function () { + beforeEach(() => graph.addLink(firstNode.id, secondNode.id)); + + it("adds the link to the first node", function () { + const retrievedNode = graph.getNode(firstNode.id); + assert.exists(retrievedNode); + assert.include( + [...retrievedNode?.links.values()!], + secondNode.id + ); + }); + + it("does not add the backlink to the second node", function () { + const retrievedNode = graph.getNode(secondNode.id); + assert.exists(retrievedNode); + assert.notInclude( + [...retrievedNode?.backlinks.values()!], + secondNode.id + ); + }); + + context("when fixEdges is called on the graph", function () { + let retrievedFirstNode: Node; + let retrievedSecondNode: Node; + beforeEach(() => { + graph.fixEdges(); + retrievedFirstNode = graph.getNode(firstNode.id)!; + retrievedSecondNode = graph.getNode(secondNode.id)!; + }); + + it("does not remove the valid link", function () { + assert.include( + [...retrievedFirstNode!.links.values()], + secondNode.id + ); + }); + + it("adds a backlink to the second node", function () { + assert.include( + [...retrievedSecondNode.backlinks.values()], + firstNode.id + ); + }); + }); + + context( + "when clearNodeLinks is called for the first node", + function () { + let retrievedFirstNode: Node; + let retrievedSecondNode: Node; + beforeEach(() => { + graph.clearNodeLinks(firstNode.id); + retrievedFirstNode = graph.getNode(firstNode.id)!; + retrievedSecondNode = graph.getNode(secondNode.id)!; + }); + + it("removes all links from the first node", function () { + assert.equal(retrievedFirstNode!.links.size, 0); + }); + + it("removes the backlink from the first node", function () { + assert.notInclude( + [...retrievedSecondNode!.backlinks.values()], + firstNode.id + ); + }); + } + ); + } + ); + }); + }); + }); +});