Skip to content

Commit

Permalink
fix: add unit testing
Browse files Browse the repository at this point in the history
  • Loading branch information
narol1024 committed Jun 2, 2024
1 parent 2a78679 commit a50bdd8
Show file tree
Hide file tree
Showing 28 changed files with 128 additions and 67 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ dist
.idea/
.vscode/
tools/dev
.DS_Store
9 changes: 6 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import 'dotenv/config';
require('dotenv/config');

const isCI = process.env.CI === 'true';

export default {
module.exports = {
verbose: true,
collectCoverage: false,
resetModules: true,
restoreMocks: true,
testEnvironment: 'node',
transform: {},
transform: {
'^.+\\\\\\\\.tsx?$': 'ts-jest',
},
preset: 'ts-jest/presets/default-esm',
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
// testMatch: ['<rootDir>/src/cli/*.spec.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
{
"name": "sdp-analyzer",
"type": "module",
"main": "dist/index.js",
"bin": "dist/cli/index.js",
"bin": "dist/bin/cli.js",
"types": "dist/main.d.ts",
"version": "1.0.0",
"scripts": {
"prepare": "husky install",
"build": "tsc --project tsconfig.build.json",
"build:clean": "rm -rf tsconfig.build.tsbuildinfo && rm -rf ./dist",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
"test": "jest",
"test:coverage": "npm run test -- --coverage",
"test:ci": "npm run test -- --colors --coverage --ci --coverageReporters=\"json-summary\"",
"lint": "eslint --ext .ts,.js .",
Expand Down
4 changes: 2 additions & 2 deletions src/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Main Tests can analyze a valid npm package 1`] = `
exports[`Main Tests can be analyzed for a valid npm package 1`] = `
Array [
Object {
"fanIn": 10000,
Expand All @@ -12,7 +12,7 @@ Array [
]
`;

exports[`Main Tests can analyze for local packages 1`] = `
exports[`Main Tests can be analyzed for local packages 1`] = `
Array [
Object {
"fanIn": 1,
Expand Down
5 changes: 5 additions & 0 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { bootstrap } from '../cli';

bootstrap();
29 changes: 29 additions & 0 deletions src/cli/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* This is a sample test suite.
* Replace this with your implementation.
*/
import { bootstrap } from './index';

describe('CLI tool Tests', () => {
it('main', async () => {
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']);
const result = spy.mock.calls[0][0];
expect(result).toEqual(
JSON.stringify(
[
{
name: 'react',
fanIn: 10000,
fanOut: 1,
stability: 0.00009999000099990002,
label: 'Stable',
},
],
null,
2,
),
);
});
});
24 changes: 13 additions & 11 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { analyze } from '../index';

const program = new Command();

program.name('sdp-analyzer').description('A cli tool for analyzing package stability').version('1.0.0');
program
.command('analyze')
.argument('<string>', 'the local package path or a npm package name')
.action(async target => {
const result = await analyze(target);
console.log(JSON.stringify(result, null, 2));
process.exit(0);
});
async function bootstrap(argv = process.argv) {
program.name('sdp-analyzer').description('A cli tool for analyzing package stability').version('1.0.0');
program
.command('analyze')
.argument('<string>', 'the local package path or a npm package name')
.action(async target => {
const result = await analyze(target);
console.log(JSON.stringify(result, null, 2));
});
await program.parseAsync(argv);
return program;
}

program.parse();
export { bootstrap };
20 changes: 18 additions & 2 deletions src/core/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@
import { evaluate } from './index';

describe('Core Tests', () => {
it('should be return 0.2', () => {
expect(evaluate(1, 4)).toEqual(0.2);
test('should return correct SDP value with non-zero fan-in and fan-out', () => {
expect(evaluate(4, 4)).toBe(0.5);
});

test('should return 1 when fan-in is 0', () => {
expect(evaluate(5, 0)).toBe(1);
});

test('should return 0 when fan-out is 0', () => {
expect(evaluate(0, 3)).toBe(0);
});

test('should return 0 when both fan-in and fan-out are 0', () => {
expect(evaluate(0, 0)).toBe(0);
});

test('should handle large numbers', () => {
expect(evaluate(1000, 500)).toBe(2 / 3);
});
});
6 changes: 3 additions & 3 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import { analyze } from './index';

describe('Main Tests', () => {
it('can analyze a valid npm package', async () => {
it('can be analyzed for a valid npm package', async () => {
await expect(analyze('react')).resolves.toMatchSnapshot();
});
it('can analyze for local packages', async () => {
await expect(analyze('./src/platforms/workspaces/yarn/fixture')).resolves.toMatchSnapshot();
it('can be analyzed for local packages', async () => {
await expect(analyze('./src/platforms/workspaces/yarn/fixture/valid-packages')).resolves.toMatchSnapshot();
});
});
6 changes: 3 additions & 3 deletions src/platforms/npm-package/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Npm pacakge Tests can analyze for multiple packages 1`] = `
exports[`Npm pacakge Tests can be analyzed for multiple packages 1`] = `
Array [
Object {
"fanIn": 10000,
Expand All @@ -19,7 +19,7 @@ Array [
]
`;

exports[`Npm pacakge Tests can analyze for multiple packages with the package version 1`] = `
exports[`Npm pacakge Tests can be analyzed for multiple packages with the package version 1`] = `
Array [
Object {
"fanIn": 10000,
Expand All @@ -38,7 +38,7 @@ Array [
]
`;

exports[`Npm pacakge Tests can analyze for single package 1`] = `
exports[`Npm pacakge Tests can be analyzed for single package 1`] = `
Array [
Object {
"fanIn": 10000,
Expand Down
6 changes: 3 additions & 3 deletions src/platforms/npm-package/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ describe('Npm pacakge Tests', () => {
{ name: 'loose-envify', version: '^1.1.0' },
]);
});
it('can analyze for single package', async () => {
it('can be analyzed for single package', async () => {
await expect(analyze('react')).resolves.toMatchSnapshot();
});
it('can analyze for multiple packages', async () => {
it('can be analyzed for multiple packages', async () => {
await expect(analyze('react,vue')).resolves.toMatchSnapshot();
});
it('can analyze for multiple packages with the package version', async () => {
it('can be analyzed for multiple packages with the package version', async () => {
await expect(analyze('[email protected],[email protected]')).resolves.toMatchSnapshot();
});
});
38 changes: 19 additions & 19 deletions src/platforms/npm-package/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { load as cheerioLoad } from 'cheerio';
import type { Dep, NpmDependency } from '../../types';
import { evaluate } from '../../core';
import { getStablityLabel } from '../../utils/label';
import { timeout } from '../../utils/timeout';

const execAsync = promisify(exec);

Expand All @@ -16,6 +15,9 @@ export async function readNpmPackageDependencies(packageName: string): Promise<N
return new Promise((resolve, reject) => {
// Relying on remote network is unstable possibly, it is necessary to use a task for retries.
async function doReadTask(): Promise<void> {
const timeoutPromise = new Promise<void>((_, timeoutReject) => {
readNpmPackageDependenciesTimeId = setTimeout(() => timeoutReject(new Error('Operation timed out')), 10000);
});
const npmViewPromise = execAsync(`npm view ${packageName} dependencies --json`).then(result => {
try {
const dependencies = JSON.parse(result.stdout) as NpmDependency;
Expand All @@ -31,9 +33,9 @@ export async function readNpmPackageDependencies(packageName: string): Promise<N
clearTimeout(readNpmPackageDependenciesTimeId);
}
});
Promise.race([npmViewPromise, timeout(10000, readNpmPackageDependenciesTimeId)])
Promise.race([npmViewPromise, timeoutPromise])
.catch(async error => {
if (error.message === 'Operation timed out' && retryTimes < 3) {
if (error.message === 'Operation timed out' && retryTimes < 10) {
retryTimes += 1;
doReadTask();
} else {
Expand All @@ -58,6 +60,9 @@ export async function countNpmPackageDependants(packageName: string, version: st
async function doCountTask(): Promise<void> {
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')), 10000);
});
const fetchPromise = async (): Promise<void> => {
try {
const res = await nodeFetch(url);
Expand All @@ -81,9 +86,9 @@ export async function countNpmPackageDependants(packageName: string, version: st
clearTimeout(countNpmPackageDependantsTimeId);
}
};
Promise.race([fetchPromise(), timeout(10000, countNpmPackageDependantsTimeId)])
Promise.race([fetchPromise(), timeoutPromise])
.catch(async error => {
if ((error.message === 'Operation timed out' || error.message === 'Operation failed') && retryTimes < 5) {
if ((error.message === 'Operation timed out' || error.message === 'Operation failed') && retryTimes < 6) {
retryTimes += 1;
doCountTask();
} else {
Expand All @@ -101,33 +106,28 @@ export async function countNpmPackageDependants(packageName: string, version: st
// To analyze one or multiple packages on the npm repository, like react, vue, express, etc.
export async function analyze(packageNames: string): Promise<Dep[]> {
try {
// default to npm package
const normalizedPackageNames = packageNames.split(',');
const deps: Dep[] = [];
for (let packageName of normalizedPackageNames) {
const depsPromises = normalizedPackageNames.map(async packageName => {
let version = null;
if (packageName.includes('@')) {
const _arr = packageName.split('@');
packageName = _arr[0].trim();
version = _arr[1];
[packageName, version] = packageName.split('@').map(part => part.trim());
}
const [dependencies, dependants] = await Promise.all([
readNpmPackageDependencies(packageName),
countNpmPackageDependants(packageName, version),
]);
const _dep = {
const fanOut = dependencies.length;
const stability = fanOut === 0 ? 0 : evaluate(fanOut, dependants);
return {
name: packageName,
fanIn: dependants,
fanOut: dependencies.length,
};
const stability = _dep.fanOut === 0 ? 0 : evaluate(_dep.fanOut, _dep.fanIn);
const dep = {
..._dep,
fanOut,
stability,
label: getStablityLabel(stability),
};
deps.push(dep);
}
});

const deps = await Promise.all(depsPromises);
return Promise.resolve(deps);
} catch (error) {
return Promise.reject(new Error(`Cannot analyze ${packageNames}`));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Workspaces Tests can analyze for local packages 1`] = `
exports[`Workspaces Tests can be analyzed for the local packages 1`] = `
Array [
Object {
"fanIn": 1,
Expand Down Expand Up @@ -40,7 +40,7 @@ Array [
]
`;

exports[`Workspaces Tests should be that the package.json can be found 1`] = `
exports[`Workspaces Tests should read a valid package 1`] = `
Array [
Object {
"dependencies": Object {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
18 changes: 14 additions & 4 deletions src/platforms/workspaces/yarn/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ describe('Workspaces Tests', () => {
readWorkspacesDependencies('./error-path');
}).toThrow(new Error(`Cannot find package.json on ./error-path.`));
});
it('should be that the package.json can be found', () => {
expect(readWorkspacesDependencies('./src/platforms/workspaces/yarn/fixture')).toMatchSnapshot();
it('should read a valid package', () => {
expect(readWorkspacesDependencies('./src/platforms/workspaces/yarn/fixture/valid-packages')).toMatchSnapshot();
});
it('can analyze for local packages', async () => {
await expect(analyze('./src/platforms/workspaces/yarn/fixture')).resolves.toMatchSnapshot();
it('should not read an invalid package', async () => {
await expect(analyze('./src/platforms/workspaces/yarn/fixture/invalid-packages')).rejects.toEqual(
new Error(
`It seems that the 'subpackages' directory has not been found, please check the configuration of yarn workspaces..`,
),
);
});
it('can not analyze for the invalid local packages', async () => {
await expect(analyze('./error-path')).rejects.toEqual(new Error(`Cannot analyze ./error-path`));
});
it('can be analyzed for the local packages', async () => {
await expect(analyze('./src/platforms/workspaces/yarn/fixture/valid-packages')).resolves.toMatchSnapshot();
});
});
9 changes: 7 additions & 2 deletions src/platforms/workspaces/yarn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export function readWorkspacesDependencies(packagePath: string) {
try {
const rootPackageJson = jsonfile.readFileSync(packageJsonPath);
const workspaces = rootPackageJson.workspaces as string[];
if (workspaces === undefined || workspaces.length === 0) {
return [];
}
const dependencyMap = fg
.sync(
workspaces.map(v => `${v}/*.json`),
Expand All @@ -35,8 +38,10 @@ export async function analyze(packagePath: string) {
try {
const dependencyMap = readWorkspacesDependencies(packagePath);
if (dependencyMap.length === 0) {
throw new Error(
"It seems that the 'subpackages' directory has not been found, please check the configuration of yarn workspaces..",
return Promise.reject(
new Error(
"It seems that the 'subpackages' directory has not been found, please check the configuration of yarn workspaces..",
),
);
}
const deps = dependencyMap.map(v => {
Expand Down
10 changes: 0 additions & 10 deletions src/utils/timeout.ts

This file was deleted.

0 comments on commit a50bdd8

Please sign in to comment.