From 348d93fc65dcbc5b7ca01c43dafac1335ac608f6 Mon Sep 17 00:00:00 2001 From: narol Date: Mon, 3 Jun 2024 15:32:45 +0800 Subject: [PATCH] fix: clean unit testing code --- README.md | 4 + jest-global-mock.ts | 53 +++++++++++ jest.config.js | 3 +- src/cli/index.spec.ts | 22 ----- src/index.spec.ts | 26 ----- .../__snapshots__/index.spec.ts.snap | 26 +++-- src/platforms/npm-package/index.spec.ts | 94 +++---------------- src/platforms/npm-package/index.ts | 51 +++++----- 8 files changed, 117 insertions(+), 162 deletions(-) create mode 100644 jest-global-mock.ts diff --git a/README.md b/README.md index 4bfaad6..61bc65b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # SDP-Analyzer [![cov](https://narol1024.github.io/sdp-analyzer/badges/coverage.svg)](https://github.com/narol1024/sdp-analyzer/actions) +[![npm version](https://img.shields.io/npm/v/sdp-analyzer.svg?style=flat)](https://www.npmjs.com/package/sdp-analyzer) +[![npm](https://img.shields.io/npm/dm/sdp-analyzer.svg)](https://www.npmjs.com/package/sdp-analyzer) +[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat)](https://github.com/narol1024/sdp-analyzer/blob/main/LICENSE) + An analyzer to implement the SDP (Stable Dependencies Principle) theory. You can quickly analyze your npm package here: https://narol.pro/sdp-analyzer diff --git a/jest-global-mock.ts b/jest-global-mock.ts new file mode 100644 index 0000000..0fa534d --- /dev/null +++ b/jest-global-mock.ts @@ -0,0 +1,53 @@ +/** + * This is a sample test suite. + * Replace this with your implementation. + */ +import { exec } from 'child_process'; +import fetch from 'node-fetch'; + +jest.mock('child_process'); +jest.mock('node-fetch'); + +const dependantsHtml = ` + + + 10000 Dependents + + +`; + +(exec as any).mockImplementation((command: any, callback: any) => { + if (command.includes('is-a-non-existent-package')) { + callback(null, { stdout: '{"error": { "code": "E404" }}' }); + } else if (/react@\d+\.\d+\.\d+/.test(command)) { + callback(null, { + stdout: `{ "fbjs": "^0.8.16", "prop-types": "^15.6.0", "loose-envify": "^1.1.0", "object-assign": "^4.1.1" }`, + }); + } else if (/vue@\d+\.\d+\.\d+/.test(command)) { + callback(null, { + stdout: `{ "@vue/shared": "3.0.0", "@vue/compiler-dom": "3.0.0", "@vue/runtime-dom": "3.0.0" }`, + }); + } else if (/\sreact\s/.test(command)) { + callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); + } else if (/\svue\s/.test(command)) { + callback(null, { + stdout: `{ "@vue/shared": "3.4.27", "@vue/compiler-dom": "3.4.27", "@vue/compiler-sfc": "3.4.27", "@vue/runtime-dom": "3.4.27", "@vue/server-renderer": "3.4.27" }`, + }); + } else { + callback(null, { stdout: '{}' }); + } +}); + +(fetch as any).mockImplementation((args: any) => { + // It returns 404 status when fetching a invalid package. + if (args.includes('is-a-non-existent-package')) { + return Promise.resolve({ + status: 404, + text: () => '', + }); + } + return Promise.resolve({ + status: 200, + text: () => dependantsHtml, + }); +}); diff --git a/jest.config.js b/jest.config.js index 934d8f7..9a94480 100644 --- a/jest.config.js +++ b/jest.config.js @@ -36,5 +36,6 @@ module.exports = { ], coverageProvider: 'v8', coverageReporters: isCI ? ['json'] : ['text'], - testTimeout: 120 * 1000, + testTimeout: 60 * 1000, + setupFiles: ['./jest-global-mock.ts'], }; diff --git a/src/cli/index.spec.ts b/src/cli/index.spec.ts index 1d7bb20..2765911 100644 --- a/src/cli/index.spec.ts +++ b/src/cli/index.spec.ts @@ -2,32 +2,10 @@ * This is a sample test suite. * Replace this with your implementation. */ -import fetch from 'node-fetch'; -import { exec } from 'child_process'; import { bootstrap } from './index'; -const dependantsHtml = ` - - - 10000 Dependents - - -`; - -jest.mock('child_process'); -jest.mock('node-fetch'); - describe('CLI tool Tests', () => { it('main', async () => { - (exec as any).mockImplementationOnce((_: any, callback: any) => { - callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); - }); - (fetch as any).mockReturnValueOnce( - Promise.resolve({ - status: 200, - text: () => dependantsHtml, - }), - ); const spy = jest.spyOn(console, 'log'); const program = await bootstrap(['node', 'index.js', 'analyze', 'react']); await expect(program.commands.map((i: any) => i._name)).toEqual(['analyze']); diff --git a/src/index.spec.ts b/src/index.spec.ts index 6d01643..ef33844 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,33 +1,7 @@ -/** - * This is a sample test suite. - * Replace this with your implementation. - */ -import fetch from 'node-fetch'; -import { exec } from 'child_process'; import { analyze } from './index'; -jest.mock('child_process'); -jest.mock('node-fetch'); - -const dependantsHtml = ` - - - 10000 Dependents - - -`; - describe('Main Tests', () => { it('can be analyzed for a valid npm package', async () => { - (exec as any).mockImplementationOnce((_: any, callback: any) => { - callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); - }); - (fetch as any).mockReturnValueOnce( - Promise.resolve({ - status: 200, - text: () => dependantsHtml, - }), - ); await expect(analyze('react')).resolves.toMatchSnapshot(); }); it('can be analyzed for local packages', async () => { diff --git a/src/platforms/npm-package/__snapshots__/index.spec.ts.snap b/src/platforms/npm-package/__snapshots__/index.spec.ts.snap index 5ae0094..fdb4a07 100644 --- a/src/platforms/npm-package/__snapshots__/index.spec.ts.snap +++ b/src/platforms/npm-package/__snapshots__/index.spec.ts.snap @@ -1,6 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Npm pacakge Tests can be analyzed for multiple packages 1`] = ` +exports[`Npm package tests can be analyzed for a zero dependency library 1`] = ` +Array [ + Object { + "fanIn": 10000, + "fanOut": 0, + "label": "Stable", + "name": "lodash", + "stability": 0, + }, +] +`; + +exports[`Npm package tests can be analyzed for multiple packages 1`] = ` Array [ Object { "fanIn": 10000, @@ -19,26 +31,26 @@ Array [ ] `; -exports[`Npm pacakge Tests can be analyzed for multiple packages with the package version 1`] = ` +exports[`Npm package tests can be analyzed for multiple packages with the package version 1`] = ` Array [ Object { "fanIn": 10000, - "fanOut": 4, + "fanOut": 1, "label": "Stable", "name": "react", - "stability": 0.00039984006397441024, + "stability": 0.00009999000099990002, }, Object { "fanIn": 10000, - "fanOut": 3, + "fanOut": 5, "label": "Stable", "name": "vue", - "stability": 0.00029991002699190244, + "stability": 0.0004997501249375312, }, ] `; -exports[`Npm pacakge Tests can be analyzed for single package 1`] = ` +exports[`Npm package tests can be analyzed for single package 1`] = ` Array [ Object { "fanIn": 10000, diff --git a/src/platforms/npm-package/index.spec.ts b/src/platforms/npm-package/index.spec.ts index 599ccc9..8411be6 100644 --- a/src/platforms/npm-package/index.spec.ts +++ b/src/platforms/npm-package/index.spec.ts @@ -2,111 +2,45 @@ * This is a sample test suite. * Replace this with your implementation. */ -import fetch from 'node-fetch'; -import { exec } from 'child_process'; import { readNpmPackageDependencies, countNpmPackageDependants, analyze } from './index'; -const dependantsHtml = ` - - - 10000 Dependents - - -`; - -jest.mock('child_process'); -jest.mock('node-fetch'); - -// TODO: -// describe('Npm pacakge Tests for the real world', () => { -// it('can be analyzed for single package', async () => { -// await expect(analyze('react')).resolves.toMatchSnapshot(); -// }); -// }); - -describe('Npm pacakge Tests', () => { +describe('Npm package tests', () => { it('should not read a non-existent package', async () => { - (exec as any).mockImplementationOnce((_: any, callback: any) => { - callback(null, { stdout: '{"error": { "code": "E404" }}' }); - }); await expect(readNpmPackageDependencies('is-a-non-existent-package')).rejects.toEqual( new Error('Cannot read is-a-non-existent-package.'), ); }); it('should read a valid package', async () => { - (exec as any).mockImplementationOnce((_: any, callback: any) => { - callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); - }); - await expect(readNpmPackageDependencies('react@18.3.1')).resolves.toEqual([ + await expect(readNpmPackageDependencies('react@16.0.0')).resolves.toEqual([ + { name: 'fbjs', version: '^0.8.16' }, + { name: 'prop-types', version: '^15.6.0' }, { name: 'loose-envify', version: '^1.1.0' }, + { name: 'object-assign', version: '^4.1.1' }, ]); }); it('should count the depandants of a valid package', async () => { - (fetch as any).mockReturnValueOnce( - Promise.resolve({ - status: 200, - text: () => dependantsHtml, - }), - ); - await expect(countNpmPackageDependants('react', '18.3.1')).resolves.toEqual(10000); + await expect(countNpmPackageDependants('react', '16.0.0')).resolves.toEqual(10000); }); it('can not count the depandants of a invalid package', async () => { - (fetch as any).mockReturnValueOnce( - Promise.resolve({ - status: 200, - text: () => '', - }), - ); await expect(countNpmPackageDependants('is-a-non-existent-package', '1.0.0')).rejects.toEqual( new Error(`Cannot count the depandants of the is-a-non-existent-package.`), ); }); - it('can be analyzed for single package', async () => { - (exec as any).mockImplementationOnce((_: any, callback: any) => { - callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); - }); - (fetch as any).mockReturnValueOnce( - Promise.resolve({ - status: 200, - text: () => dependantsHtml, - }), + it('can be analyzed for a invalid package', async () => { + await expect(analyze('is-a-non-existent-package')).rejects.toEqual( + new Error(`Cannot analyze is-a-non-existent-package, the reason is "Cannot read is-a-non-existent-package."`), ); + }); + it('can be analyzed for single package', async () => { await expect(analyze('react')).resolves.toMatchSnapshot(); }); it('can be analyzed for multiple packages', async () => { - (exec as any).mockImplementation((command: string, callback: any) => { - if (command.includes('react')) { - callback(null, { stdout: '{ "loose-envify": "^1.1.0" }' }); - } else if (command.includes('vue')) { - callback(null, { - stdout: `{ "@vue/shared": "3.4.27", "@vue/compiler-dom": "3.4.27", "@vue/compiler-sfc": "3.4.27", "@vue/runtime-dom": "3.4.27", "@vue/server-renderer": "3.4.27" }`, - }); - } else { - callback(null, { stdout: '{}' }); - } - }); - (fetch as any).mockReturnValue( - Promise.resolve({ - status: 200, - text: () => dependantsHtml, - }), - ); await expect(analyze('react,vue')).resolves.toMatchSnapshot(); }); + it('can be analyzed for a zero dependency library', async () => { + await expect(analyze('lodash@5.0.0')).resolves.toMatchSnapshot(); + }); it('can be analyzed for multiple packages with the package version', async () => { - (exec as any).mockImplementation((command: string, callback: any) => { - if (command.includes('react')) { - callback(null, { - stdout: `{ "fbjs": "^0.8.16", "prop-types": "^15.6.0", "loose-envify": "^1.1.0", "object-assign": "^4.1.1" }`, - }); - } else if (command.includes('vue')) { - callback(null, { - stdout: `{ "@vue/shared": "3.0.0", "@vue/compiler-dom": "3.0.0", "@vue/runtime-dom": "3.0.0" }`, - }); - } else { - callback(null, { stdout: '{}' }); - } - }); await expect(analyze('react@16.0.0,vue@3.0.0')).resolves.toMatchSnapshot(); }); }); diff --git a/src/platforms/npm-package/index.ts b/src/platforms/npm-package/index.ts index a44b3cd..fa51455 100644 --- a/src/platforms/npm-package/index.ts +++ b/src/platforms/npm-package/index.ts @@ -18,27 +18,28 @@ export async function readNpmPackageDependencies(packageName: string): Promise((_, timeoutReject) => { readNpmPackageDependenciesTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 5000); }); - const npmViewPromise = execAsync(`npm view ${packageName} dependencies --json`).then(result => { - try { - const resultJson = JSON.parse(result.stdout) as any; - if (!!resultJson.error) { - reject(new Error(`Cannot read ${packageName}.`)); + const npmViewPromise = execAsync(`npm view ${packageName} dependencies --json`).then( + (result: any): Promise => { + try { + const resultJson = JSON.parse(result.stdout) as any; + if (!!resultJson.error) { + throw new Error(`Cannot read ${packageName}.`); + } + resolve( + Object.entries(resultJson as NpmDependency).map(([name, version]) => ({ + name, + version, + })), + ); + return Promise.resolve(); + } catch (error) { + return Promise.reject(new Error(`Cannot read ${packageName}.`)); } - resolve( - Object.entries(resultJson as NpmDependency).map(([name, version]) => ({ - name, - version, - })), - ); - } catch (error) { - reject(new Error(`Cannot read ${packageName}.`)); - } finally { - clearTimeout(readNpmPackageDependenciesTimeId); - } - }); + }, + ); Promise.race([npmViewPromise, timeoutPromise]) - .catch(async error => { - if (error.message === 'Operation timed out' && retriedTimes < 100) { + .catch(() => { + if (retriedTimes < 5) { retriedTimes += 1; doReadTask(); } else { @@ -64,7 +65,7 @@ export async function countNpmPackageDependants(packageName: string, version: st const path = version !== null ? `${packageName}/v/${version}` : packageName; const url = `https://www.npmjs.com/package/${path}?activeTab=dependents&t=${Date.now()}`; const timeoutPromise = new Promise((_, timeoutReject) => { - countNpmPackageDependantsTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 20000); + countNpmPackageDependantsTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 5000); }); const fetchPromise = async (): Promise => { try { @@ -75,18 +76,16 @@ export async function countNpmPackageDependants(packageName: string, version: st const match = htmlStr?.match(/(?<=<\/svg>\s*)(\S+)(?=\s*Dependents)/)?.[0]; if (match) { resolve(parseInt(match.replace(/,/g, ''), 10)); - } else { - return Promise.reject(new Error(`Operation failed`)); + return Promise.resolve(); } + throw new Error(`Operation failed`); } catch (error) { return Promise.reject(new Error(`Operation failed`)); - } finally { - clearTimeout(countNpmPackageDependantsTimeId); } }; Promise.race([fetchPromise(), timeoutPromise]) - .catch(async error => { - if ((error.message === 'Operation timed out' || error.message === 'Operation failed') && retriedTimes < 100) { + .catch(() => { + if (retriedTimes < 5) { retriedTimes += 1; doCountTask(); } else {