diff --git a/src/core/dep-graph.ts b/src/core/dep-graph.ts index b95cc33..cdd4b5b 100644 --- a/src/core/dep-graph.ts +++ b/src/core/dep-graph.ts @@ -152,9 +152,14 @@ class DepGraphImpl implements types.DepGraphInternal { public countPathsToRoot(pkg: types.Pkg): number { let count = 0; for (const nodeId of this.getPkgNodeIds(pkg)) { - count += this.countNodePathsToRoot(nodeId); + if (this._countNodePathsToRootCache.has(nodeId)) { + count += this._countNodePathsToRootCache.get(nodeId)!; + } else { + const c = this.countNodePathsToRoot(nodeId); + this._countNodePathsToRootCache.set(nodeId, c); + count += c; + } } - return count; } @@ -367,30 +372,17 @@ class DepGraphImpl implements types.DepGraphInternal { return allPaths; } - private countNodePathsToRoot( - nodeId: string, - ancestors: string[] = [], - ): number { - if (ancestors.includes(nodeId)) { - return 0; - } - - if (this._countNodePathsToRootCache.has(nodeId)) { - return this._countNodePathsToRootCache.get(nodeId) || 0; - } - - const parentNodesIds = this.getNodeParentsNodeIds(nodeId); - if (parentNodesIds.length === 0) { - this._countNodePathsToRootCache.set(nodeId, 1); + private countNodePathsToRoot(nodeId: string, visited: string[] = []): number { + if (nodeId === this._rootNodeId) { return 1; } - - ancestors = ancestors.concat(nodeId); - const count = parentNodesIds.reduce((acc, parentNodeId) => { - return acc + this.countNodePathsToRoot(parentNodeId, ancestors); - }, 0); - - this._countNodePathsToRootCache.set(nodeId, count); + visited = visited.concat(nodeId); + let count = 0; + for (const parentNodeId of this.getNodeParentsNodeIds(nodeId)) { + if (!visited.includes(parentNodeId)) { + count += this.countNodePathsToRoot(parentNodeId, visited); + } + } return count; } } diff --git a/test/core/__snapshots__/cycles.test.ts.snap b/test/core/__snapshots__/cycles.test.ts.snap deleted file mode 100644 index 9108f37..0000000 --- a/test/core/__snapshots__/cycles.test.ts.snap +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`cycles pkgPathsToRoot - should work with cycles: bar@3 1`] = ` -Array [ - Array [ - Object { - "name": "bar", - "version": "3", - }, - Object { - "name": "foo", - "version": "2", - }, - Object { - "name": "toor", - "version": "1.0.0", - }, - ], -] -`; - -exports[`cycles pkgPathsToRoot - should work with cycles: baz@4 1`] = ` -Array [ - Array [ - Object { - "name": "baz", - "version": "4", - }, - Object { - "name": "foo", - "version": "2", - }, - Object { - "name": "toor", - "version": "1.0.0", - }, - ], - Array [ - Object { - "name": "baz", - "version": "4", - }, - Object { - "name": "bar", - "version": "3", - }, - Object { - "name": "foo", - "version": "2", - }, - Object { - "name": "toor", - "version": "1.0.0", - }, - ], -] -`; - -exports[`cycles pkgPathsToRoot - should work with cycles: foo@2 1`] = ` -Array [ - Array [ - Object { - "name": "foo", - "version": "2", - }, - Object { - "name": "toor", - "version": "1.0.0", - }, - ], -] -`; - -exports[`cycles pkgPathsToRoot - should work with cycles: toor@1.0.0 1`] = ` -Array [ - Array [ - Object { - "name": "toor", - "version": "1.0.0", - }, - ], -] -`; diff --git a/test/core/__snapshots__/pkg-paths-to-root.test.ts.snap b/test/core/__snapshots__/pkg-paths-to-root.test.ts.snap new file mode 100644 index 0000000..083d533 --- /dev/null +++ b/test/core/__snapshots__/pkg-paths-to-root.test.ts.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: a@1 1`] = ` +Array [ + Array [ + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: b@2 1`] = ` +Array [ + Array [ + Object { + "name": "b", + "version": "2", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: c@3 1`] = ` +Array [ + Array [ + Object { + "name": "c", + "version": "3", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: d@4 1`] = ` +Array [ + Array [ + Object { + "name": "d", + "version": "4", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: e@5 1`] = ` +Array [ + Array [ + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "b", + "version": "2", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "c", + "version": "3", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "g", + "version": "7", + }, + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "d", + "version": "4", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: f@6 1`] = ` +Array [ + Array [ + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "d", + "version": "4", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "b", + "version": "2", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "c", + "version": "3", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; + +exports[`pkgPathsToRoot cycles returns expected paths for all packages: g@7 1`] = ` +Array [ + Array [ + Object { + "name": "g", + "version": "7", + }, + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "d", + "version": "4", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "g", + "version": "7", + }, + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "b", + "version": "2", + }, + Object { + "name": "a", + "version": "1", + }, + ], + Array [ + Object { + "name": "g", + "version": "7", + }, + Object { + "name": "f", + "version": "6", + }, + Object { + "name": "e", + "version": "5", + }, + Object { + "name": "c", + "version": "3", + }, + Object { + "name": "a", + "version": "1", + }, + ], +] +`; diff --git a/test/core/count-paths-to-root.test.ts b/test/core/count-paths-to-root.test.ts new file mode 100644 index 0000000..08f7926 --- /dev/null +++ b/test/core/count-paths-to-root.test.ts @@ -0,0 +1,30 @@ +import * as depGraphLib from '../../src'; +import * as helpers from '../helpers'; + +describe('countPathsToRoot', () => { + const depGraphData = helpers.loadFixture('cyclic-complex-dep-graph.json'); + const depGraph = depGraphLib.createFromJSON(depGraphData); + + it('returns 1 for the root node', () => { + expect(depGraph.countPathsToRoot(depGraph.rootPkg)).toBe(1); + }); + + it.each` + name | version | expected + ${'a'} | ${'1'} | ${1} + ${'b'} | ${'2'} | ${1} + ${'c'} | ${'3'} | ${1} + ${'d'} | ${'4'} | ${1} + ${'e'} | ${'5'} | ${3} + ${'f'} | ${'6'} | ${3} + ${'g'} | ${'7'} | ${3} + `('returns $expected for $name@$version', ({ name, version, expected }) => { + expect(depGraph.countPathsToRoot({ name, version })).toBe(expected); + }); + + it.each(depGraph.getPkgs())(`equals pkgPathsToRoot(%s).length`, (pkg) => { + expect(depGraph.countPathsToRoot(pkg)).toBe( + depGraph.pkgPathsToRoot(pkg).length, + ); + }); +}); diff --git a/test/core/cycles.test.ts b/test/core/cycles.test.ts deleted file mode 100644 index b17efd5..0000000 --- a/test/core/cycles.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as depGraphLib from '../../src'; -import * as helpers from '../helpers'; - -describe('cycles', () => { - let depGraph; - let toor; - let foo; - let bar; - let baz; - - beforeEach(() => { - const cyclicDepGraphData = helpers.loadFixture('cyclic-dep-graph.json'); - cyclicDepGraphData.graph.nodes - .find((x) => x.nodeId === 'foo@2|x') - .deps.push({ nodeId: 'baz@4|x' }); - depGraph = depGraphLib.createFromJSON(cyclicDepGraphData); - [toor, foo, bar, baz] = depGraph.getPkgs(); - }); - - test('pkgPathsToRoot - should work with cycles', () => { - depGraph.getPkgs().forEach((pkg) => { - const pkgPathsToRoot = depGraph.pkgPathsToRoot(pkg); - - expect(pkgPathsToRoot).toMatchSnapshot(`${pkg.name}@${pkg.version}`); - }); - }); - - describe('countPathsToRoot - should work with cycles', () => { - it('toor', () => expect(depGraph.countPathsToRoot(toor)).toBe(1)); - it('foo', () => expect(depGraph.countPathsToRoot(foo)).toBe(1)); - it('bar', () => expect(depGraph.countPathsToRoot(bar)).toBe(1)); - it('baz', () => expect(depGraph.countPathsToRoot(baz)).toBe(2)); - }); -}); diff --git a/test/core/pkg-paths-to-root.test.ts b/test/core/pkg-paths-to-root.test.ts index b46e75e..22fd362 100644 --- a/test/core/pkg-paths-to-root.test.ts +++ b/test/core/pkg-paths-to-root.test.ts @@ -81,4 +81,15 @@ describe('pkgPathsToRoot', () => { expect(pathsWithoutLimit.length).toBeGreaterThan(limit); expect(pathsWithlimit).toHaveLength(limit); }); + + describe('cycles', () => { + const depGraphData = loadFixture('cyclic-complex-dep-graph.json'); + const depGraph = createFromJSON(depGraphData); + it('returns expected paths for all packages', () => { + depGraph.getPkgs().forEach((pkg) => { + const pkgPathsToRoot = depGraph.pkgPathsToRoot(pkg); + expect(pkgPathsToRoot).toMatchSnapshot(`${pkg.name}@${pkg.version}`); + }); + }); + }); }); diff --git a/test/core/stress.test.ts b/test/core/stress.test.ts index 6257b56..9470b7f 100644 --- a/test/core/stress.test.ts +++ b/test/core/stress.test.ts @@ -36,4 +36,21 @@ describe('stress tests', () => { }); expect(result).toBeDefined(); }); + test('countPathsToRoot() is cached', async () => { + const graph = await generateLargeGraph(100_000); + let start = Date.now(); + graph.countPathsToRoot({ + name: dependencyName, + version: '1.2.3', + }); + const firstCallDuration = Date.now() - start; + start = Date.now(); + graph.countPathsToRoot({ + name: dependencyName, + version: '1.2.3', + }); + const secondCallDuration = Date.now() - start; + // the second call must be *significantly* faster than the first + expect(secondCallDuration).toBeLessThan(firstCallDuration * 0.2); + }); });