UNPKG

mako-tree

Version:

The build tree structure used internally by mako

652 lines (544 loc) 19.1 kB
/* eslint-env mocha */ 'use strict' let assert = require('chai').assert let bufferEqual = require('buffer-equal') let File = require('../lib/file') let Tree = require('../lib/tree') describe('Tree([root])', function () { it('should be a constructor function', function () { assert.instanceOf(new Tree(), Tree) }) it('should add the root property', function () { let tree = new Tree('a') assert.strictEqual(tree.root, 'a') }) it('should be empty by default', function () { let tree = new Tree() assert.equal(tree.size(), 0) }) describe('#hasFile(id)', function () { let tree = new Tree() let file = tree.addFile({ path: 'a.js' }) it('should return false for a missing node', function () { assert.isFalse(tree.hasFile('does-not-exist')) }) it('should return true for an existing node', function () { assert.isTrue(tree.hasFile(file)) }) it('should allow using a string id', function () { assert.isTrue(tree.hasFile(file.id)) }) }) describe('#addFile(params)', function () { it('should add the file to the graph', function () { let tree = new Tree() tree.addFile('a') assert.strictEqual(tree.size(), 1) }) it('should return the new file', function () { let tree = new Tree() let a = tree.addFile('a') assert.instanceOf(a, File) }) it('should set the path of the new file', function () { let tree = new Tree() let a = tree.addFile('a') assert.strictEqual(a.path, 'a') }) it('should support objects', function () { let tree = new Tree() let a = tree.addFile({ path: 'a' }) assert.strictEqual(a.path, 'a') }) context('with root', function () { it('should impose tree.root as file.base', function () { let tree = new Tree('a') let file = tree.addFile('z') assert.strictEqual(file.base, 'a') }) it('should override any specified base with tree.root', function () { let tree = new Tree('a') let file = tree.addFile({ base: 'b', path: 'z' }) assert.strictEqual(file.base, 'a') }) }) }) describe('#getFile(file)', function () { let tree = new Tree() let file = tree.addFile('index.html') it('should return a file instance', function () { assert.strictEqual(tree.getFile(file.id), file) }) it('should return undefined when the file does not exist', function () { assert.isUndefined(tree.getFile('does-not-exist')) }) }) describe('#findFile(file)', function () { let tree = new Tree() let file = tree.addFile('/path/to/index.jade') file.type = 'html' // intentionally change extension to add to history it('should return the file instance if the path matches', function () { assert.strictEqual(file, tree.findFile('/path/to/index.html')) }) it('should return the file instance if anything in the history matches', function () { assert.strictEqual(file, tree.findFile('/path/to/index.jade')) }) it('should return undefined when the file does not exist', function () { assert.isUndefined(tree.findFile('does-not-exist')) }) it('should support passing objects', function () { assert.strictEqual(file, tree.findFile({ path: '/path/to/index.html' })) }) }) describe('#getFiles([options])', function () { // index.html <- index.js <- shared.js // <- index.css <- shared.css let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') let sharedJS = tree.addFile('shared.js') let css = tree.addFile('index.css') let sharedCSS = tree.addFile('shared.css') tree.addDependency(html, js) tree.addDependency(html, css) tree.addDependency(js, sharedJS) tree.addDependency(css, sharedCSS) it('should return a list of all the files in the tree', function () { let files = tree.getFiles() assert.lengthOf(files, tree.size()) assert.sameMembers(files, [ html, js, css, sharedJS, sharedCSS ]) }) context('with options', function () { context('.topological', function () { it('should sort the results topologically', function () { assert.deepEqual(tree.getFiles({ topological: true }), [ sharedJS, sharedCSS, js, css, html ]) }) }) }) }) describe('#removeFile(file, [options])', function () { it('should remove the file from the tree', function () { let tree = new Tree() let file = tree.addFile('index.html') tree.removeFile(file) assert.isFalse(tree.hasFile(file)) }) it('should fail if there are still dependencies defined', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) assert.throws(function () { tree.removeFile(html) }) }) it('should support using a string id', function () { let tree = new Tree() let file = tree.addFile('index.html') tree.removeFile(file.id) assert.isFalse(tree.hasFile(file)) }) context('with options', function () { context('.force', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) tree.removeFile(html, { force: true }) assert.isFalse(tree.hasFile(html)) assert.isTrue(tree.hasFile(js)) }) }) }) describe('#hasDependency(parent, child)', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) it('should return false for a missing dependency', function () { assert.isFalse(tree.hasDependency(html, 'does-not-exist')) }) it('should return false when the dependency link is reversed', function () { assert.isFalse(tree.hasDependency(js, html)) }) it('should return true for an existing dependency', function () { assert.isTrue(tree.hasDependency(html, js)) }) it('should allow using a string id', function () { assert.isTrue(tree.hasDependency(html.id, js.id)) }) }) describe('#addDependency(parent, child)', function () { it('should set child as a dependency of parent', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) assert.isTrue(tree.hasDependency(html, js)) }) it('should throw if the parent was not already defined', function () { let tree = new Tree() let js = tree.addFile('index.js') assert.throws(function () { tree.addDependency('does-not-exist', js) }) }) it('should throw if the child was not already defined', function () { let tree = new Tree() let html = tree.addFile('index.html') assert.throws(function () { tree.addDependency(html, 'does-not-exist') }) }) it('should allow using string ids', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html.id, js.id) assert.isTrue(tree.hasDependency(html, js)) }) }) describe('#removeDependency(parent, child)', function () { it('should remove the edge from the graph', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) tree.removeDependency(html, js) assert.isFalse(tree.hasDependency(html, js)) }) it('should allow using string ids', function () { // index.html <- index.js let tree = new Tree() let html = tree.addFile('index.html') let js = tree.addFile('index.js') tree.addDependency(html, js) tree.removeDependency(html.id, js.id) assert.isFalse(tree.hasDependency(html, js)) }) }) describe('#dependenciesOf(file, [options])', function () { // index.js <- a.js <- b.js <- c.js let tree = new Tree() let js = tree.addFile('index.js') let a = tree.addFile('a.js') let b = tree.addFile('b.js') let c = tree.addFile('c.js') tree.addDependency(js, a) tree.addDependency(a, b) tree.addDependency(b, c) it('should return the direct dependencies of node', function () { assert.deepEqual(tree.dependenciesOf(js), [ a ]) }) it('should allow using a string id', function () { assert.deepEqual(tree.dependenciesOf(js.id), [ a ]) }) context('with options', function () { context('.recursive', function () { it('should all the dependencies of node', function () { assert.deepEqual(tree.dependenciesOf(js, { recursive: true }), [ a, b, c ]) }) it('should allow using a string id', function () { assert.deepEqual(tree.dependenciesOf(js.id, { recursive: true }), [ a, b, c ]) }) }) }) }) describe('#hasDependant(child, parent)', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependency(a, b) it('should return false for a missing dependency', function () { assert.isFalse(tree.hasDependant(b, 'does-not-exist')) }) it('should return false for a reversed dependency', function () { assert.isFalse(tree.hasDependant(a, b)) }) it('should return true for an existing dependency', function () { assert.isTrue(tree.hasDependant(b, a)) }) it('should allow using string ids', function () { assert.isTrue(tree.hasDependant(b.id, a.id)) }) }) describe('#addDependant(child, parent)', function () { it('should create an edge between the child and parent', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependant(b, a) assert.isTrue(tree.hasDependant(b, a)) }) it('should throw if the parent was not already defined', function () { let tree = new Tree() let b = tree.addFile('b.js') assert.throws(function () { tree.addDependant(b, 'does-not-exist') }) }) it('should throw if the child was not already defined', function () { let tree = new Tree() let a = tree.addFile('a.js') assert.throws(function () { tree.addDependant('does-not-exist', a) }) }) it('should support using string ids', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependant(b.id, a.id) assert.isTrue(tree.hasDependant(b, a)) }) }) describe('#removeDependant(child, parent)', function () { it('should remove the edge from the graph', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependant(b, a) tree.removeDependant(b, a) assert.isFalse(tree.hasDependant(b, a)) }) it('should allow using string ids', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependant(b, a) tree.removeDependant(b.id, a.id) assert.isFalse(tree.hasDependant(b, a)) }) }) describe('#dependantsOf(file, [options])', function () { // a.js <- b.js <- c.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') let c = tree.addFile('c.js') tree.addDependency(a, b) tree.addDependency(b, c) it('should return the direct dependants of file', function () { assert.deepEqual(tree.dependantsOf(c), [ b ]) }) it('should support using a string id', function () { assert.deepEqual(tree.dependantsOf(c.id), [ b ]) }) context('with options', function () { context('.recursive', function () { it('should all the dependencies of file', function () { assert.deepEqual(tree.dependantsOf(c, { recursive: true }), [ b, a ]) }) it('should support using a string id', function () { assert.deepEqual(tree.dependantsOf(c.id, { recursive: true }), [ b, a ]) }) }) }) }) describe('#size()', function () { // a.js <- b.js // <- c.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') let c = tree.addFile('c.js') tree.addDependency(a, b) tree.addDependency(a, c) it('should return the number of files in the tree', function () { assert.strictEqual(tree.size(), 3) }) }) describe('#clone()', function () { // a.js <- b.js // <- c.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') let c = tree.addFile('c.js') tree.addDependency(a, b) tree.addDependency(a, c) it('should make a clone of the original', function () { let clone = tree.clone() assert.notStrictEqual(tree, clone) assert.instanceOf(clone, Tree) assert.strictEqual(tree.size(), clone.size()) assert.deepEqual(tree.getFiles({ topolical: true }), clone.getFiles({ topolical: true })) }) }) describe('#prune(anchors)', function () { it('should remove all files disconnected from anchors', function () { // a* <- b // c let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') tree.addDependency(a, b) tree.prune([ a ]) assert.strictEqual(tree.size(), 2) assert.isFalse(tree.hasFile(c)) }) it('should recursively remove orphaned trees', function () { // a* <- b // c <- d let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') let d = tree.addFile('d') tree.addDependency(a, b) tree.addDependency(c, d) tree.prune([ a ]) assert.strictEqual(tree.size(), 2) assert.isFalse(tree.hasFile(c)) assert.isFalse(tree.hasFile(d)) }) it('should not remove dependencies that are still depended on elsewhere', function () { // a* <- b <- c // d <- let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') let d = tree.addFile('d') tree.addDependency(a, b) tree.addDependency(b, c) tree.addDependency(d, b) tree.prune([ a ]) assert.deepEqual(tree.getFiles({ topological: true }), [ c, b, a ]) }) it('should properly handle a complex case', function () { // a* <- b <- c <- d // e <- f <- let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') let d = tree.addFile('d') let e = tree.addFile('e') let f = tree.addFile('f') tree.addDependency(a, b) tree.addDependency(b, c) tree.addDependency(c, d) tree.addDependency(e, f) tree.addDependency(f, c) tree.prune([ a ]) assert.deepEqual(tree.getFiles({ topological: true }), [ d, c, b, a ]) }) }) describe('#removeCycles()', function () { it('should remove shallow cycles', function () { // a <-> b let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') tree.addDependency(a, b) tree.addDependency(b, a) // should be removed tree.removeCycles() assert.doesNotThrow(() => tree.getFiles({ topological: true })) }) it('should remove shallow cycles found deeper in the graph', function () { // a <- b <-> c let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') tree.addDependency(a, b) tree.addDependency(b, c) tree.addDependency(c, b) tree.removeCycles() assert.doesNotThrow(() => tree.getFiles({ topological: true })) }) it('should remove large cycles in the graph', function () { // a <- b <- c <- d // ------> let tree = new Tree() let a = tree.addFile('a') let b = tree.addFile('b') let c = tree.addFile('c') let d = tree.addFile('d') tree.addDependency(a, b) tree.addDependency(b, c) tree.addDependency(c, d) tree.addDependency(d, b) tree.removeCycles() assert.doesNotThrow(() => tree.getFiles({ topological: true })) }) }) describe('#toJSON()', function () { it('should return a list of vertices and edges for reconstructing the graph', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependency(a, b) let json = tree.toJSON() assert.isNull(json.root) assert.deepEqual(json.files, [ a, b ]) assert.deepEqual(json.dependencies, [ [ b.id, a.id ] ]) }) it('should encode a root on the object', function () { let tree = new Tree('root') let json = tree.toJSON() assert.strictEqual(json.root, tree.root) }) }) describe('#toString([space])', function () { it('should completely stringify to JSON', function () { // a.js <- b.js let tree = new Tree() let a = tree.addFile('a.js') let b = tree.addFile('b.js') tree.addDependency(a, b) assert.strictEqual(tree.toString(), JSON.stringify({ root: null, files: [ a, b ], dependencies: [ [ b.id, a.id ] ] })) }) }) describe('.fromString(input)', function () { it('should parse a JSON string into a tree instance', function () { // a <- b let tree = new Tree() let a = tree.addFile('a.js') a.contents = new Buffer('a') a.modified = new Date() let b = tree.addFile('b.js') b.contents = new Buffer('b') b.modified = new Date() tree.addDependency(a, b) let actual = Tree.fromString(tree.toString()) assert.instanceOf(actual, Tree) assert.isTrue(actual.graph.equals(tree.graph, eqV, () => true)) function eqV (a, b) { return a.path === b.path && bufferEqual(a.contents, b.contents) && dateEqual(a.modified, b.modified) } }) it('should restore the root property', function () { let tree = new Tree('root') let actual = Tree.fromString(tree.toString()) assert.strictEqual(actual.root, 'root') }) }) }) function dateEqual (a, b) { assert.instanceOf(a, Date) assert.instanceOf(b, Date) return a.getTime() === b.getTime() }