Skip to content

Commit

Permalink
Merge pull request #5 from narol1024/develop
Browse files Browse the repository at this point in the history
fix: clean unit testing code
  • Loading branch information
narol1024 authored Jun 3, 2024
2 parents 81607e5 + 348d93f commit d452d50
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 162 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
53 changes: 53 additions & 0 deletions jest-global-mock.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<html>
<body>
<a id="package-tab-dependents" tabindex="0"><span><svg></svg>10000 Dependents</span></a>
</body>
</html>
`;

(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,
});
});
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ module.exports = {
],
coverageProvider: 'v8',
coverageReporters: isCI ? ['json'] : ['text'],
testTimeout: 120 * 1000,
testTimeout: 60 * 1000,
setupFiles: ['./jest-global-mock.ts'],
};
22 changes: 0 additions & 22 deletions src/cli/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<html>
<body>
<a id="package-tab-dependents" tabindex="0"><span><svg></svg>10000 Dependents</span></a>
</body>
</html>
`;

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']);
Expand Down
26 changes: 0 additions & 26 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<html>
<body>
<a id="package-tab-dependents" tabindex="0"><span><svg></svg>10000 Dependents</span></a>
</body>
</html>
`;

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 () => {
Expand Down
26 changes: 19 additions & 7 deletions src/platforms/npm-package/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
94 changes: 14 additions & 80 deletions src/platforms/npm-package/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<html>
<body>
<a id="package-tab-dependents" tabindex="0"><span><svg></svg>10000 Dependents</span></a>
</body>
</html>
`;

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('[email protected]')).resolves.toEqual([
await expect(readNpmPackageDependencies('[email protected]')).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('[email protected]')).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('[email protected],[email protected]')).resolves.toMatchSnapshot();
});
});
51 changes: 25 additions & 26 deletions src/platforms/npm-package/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,28 @@ export async function readNpmPackageDependencies(packageName: string): Promise<N
const timeoutPromise = new Promise<void>((_, 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<void> => {
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 {
Expand All @@ -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<void>((_, timeoutReject) => {
countNpmPackageDependantsTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 20000);
countNpmPackageDependantsTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 5000);
});
const fetchPromise = async (): Promise<void> => {
try {
Expand All @@ -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 {
Expand Down

0 comments on commit d452d50

Please sign in to comment.