diff --git a/package-lock.json b/package-lock.json index c95ddcd..d030f66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.13", - "@vercel/ncc": "^0.38.4", - "esmock": "^2.7.3" + "@vercel/ncc": "^0.38.4" }, "engines": { "node": ">=20.0.0", @@ -1423,16 +1422,6 @@ "node": ">=4" } }, - "node_modules/esmock": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz", - "integrity": "sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14.16.0" - } - }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", diff --git a/package.json b/package.json index 2b0d0d1..e2d2737 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ }, "devDependencies": { "@biomejs/biome": "2.3.13", - "@vercel/ncc": "^0.38.4", - "esmock": "^2.7.3" + "@vercel/ncc": "^0.38.4" } } diff --git a/src/health-score.js b/src/health-score.js index b6425ce..62e05e4 100644 --- a/src/health-score.js +++ b/src/health-score.js @@ -3,14 +3,23 @@ import reportStatus from './report.js'; import retrieveCodeCoverage from './score_components/coverage.js'; import findProblematicComments from './score_components/find-problematic-comments.js'; +/** + * @typedef {object} HealthScoreDeps + * @property {import('@api/codecov')} codecov Codecov API client + * @property {import('./get-sha.js')} getSHA Function to get commit SHA + * @property {import('node:child_process')} childProcess Node.js child_process module + * @property {import('node:fs')} fs Node.js fs module + */ + const hs = { /** * @description Compiles the health score components * @param {import('@actions/core')} core `@actions/core` GitHub Actions core helper utility * @param {import('@actions/github')} github `@actions/github` GitHub Actions core helper utility - * @returns {Promise} score Health score details object + * @param {HealthScoreDeps} deps External dependencies + * @returns {Promise} Health score details object */ - compile: async function compileScore(core, github) { + compile: async function compileScore(core, github, deps) { // TODO: wire up action outputs const extensionInput = core.getInput('extension'); const includeInput = core.getInput('include'); @@ -21,13 +30,20 @@ const hs = { const excludes = parseYamlArray(excludeInput); let com = ''; - const misses = await hs.coverage(core, github); // uncovered LoC + const misses = await hs.coverage(core, github, deps.codecov, deps.getSHA); // uncovered LoC if (extensions.length === 0) { core.error('Extensions not specified'); } else { if (includes.length === 0) core.warning('Directories to be included not specified'); - com = hs.grep(core, extensions, includes, excludes); // to-do et al comments + com = hs.grep( + core, + deps.childProcess, + deps.fs, + extensions, + includes, + excludes, + ); // to-do et al comments } return { comments: com, diff --git a/src/index.js b/src/index.js index ff04f00..bc790a4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,15 @@ +import childProcess from 'node:child_process'; +import fs from 'node:fs'; import * as core from '@actions/core'; import * as github from '@actions/github'; +import codecov from '@api/codecov'; +import getSHA from './get-sha.js'; import hs from './health-score.js'; +const deps = { childProcess, fs, codecov, getSHA }; + const startTime = new Date(); -hs.compile(core, github) +hs.compile(core, github, deps) .then((score) => hs.report(startTime, core, github, score)) .then(console.log) .catch((err) => { diff --git a/src/score_components/coverage.js b/src/score_components/coverage.js index dfa3a82..d11d42f 100644 --- a/src/score_components/coverage.js +++ b/src/score_components/coverage.js @@ -1,13 +1,17 @@ -import codecov from '@api/codecov'; -import getSHA from '../get-sha.js'; - /** - * @description Compiles the health score components + * @description Retrieves code coverage data from Codecov API * @param {import('@actions/core')} core `@actions/core` GitHub Actions core helper utility * @param {import('@actions/github')} github `@actions/github` GitHub Actions core helper utility + * @param {import('@api/codecov')} codecov Codecov API client + * @param {import('../get-sha.js')} getSHA Function to get commit SHA * @returns {Promise} Number of uncovered lines of code, or 0 in the case of no codecov token specified */ -export default async function retrieveCodeCoverage(core, github) { +export default async function retrieveCodeCoverage( + core, + github, + codecov, + getSHA, +) { // See if we can get a coverage overview for this commit from codecov const codecovToken = core.getInput('codecov_token'); const maxAttempts = diff --git a/src/score_components/find-problematic-comments.js b/src/score_components/find-problematic-comments.js index 3af628f..5437f97 100644 --- a/src/score_components/find-problematic-comments.js +++ b/src/score_components/find-problematic-comments.js @@ -1,14 +1,23 @@ -import child_process from 'node:child_process'; -import fs from 'node:fs'; - const CommentType = Object.freeze({ TODO: 'TODO', FIXME: 'FIXME', HACK: 'HACK', }); +/** + * @description Searches for problematic comments (TODO, FIXME, HACK) in the codebase + * @param {import('@actions/core')} core `@actions/core` GitHub Actions core helper utility + * @param {import('node:child_process')} childProcess Node.js child_process module + * @param {import('node:fs')} fs Node.js fs module + * @param {string[]} ext File extensions to search + * @param {string[]} include Directories to include + * @param {string[]} exclude Directories to exclude + * @returns {Array<{path: string, line_no: number, comment: string, commentType: string|null}>} Array of problematic comments found + */ export default function grepForProblematicComments( core, + childProcess, + fs, ext, include, exclude, @@ -51,7 +60,7 @@ export default function grepForProblematicComments( find += ` -exec sh -c 'grep -EHn "${commentPattern}" "$0"' {} \\;`; let output = ''; try { - output = child_process.execSync(find).toString().trim(); + output = childProcess.execSync(find).toString().trim(); } catch (e) { core.error(e); core.error('child_process execSync failed to execute'); diff --git a/test/health-score-test.js b/test/health-score-test.js index 68d6bc1..393b8eb 100644 --- a/test/health-score-test.js +++ b/test/health-score-test.js @@ -32,7 +32,7 @@ describe('health-score', () => { exclude: 'test', }; mocks.github.context = contextValue; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.ok( mocks.core.warning.mock.calls.some((c) => c.arguments[0].includes('Directories to be included not specified'), @@ -47,7 +47,7 @@ describe('health-score', () => { exclude: '', }; mocks.github.context = contextValue; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.ok( mocks.core.error.mock.calls.some((c) => c.arguments[0].includes('Extensions not specified'), @@ -66,10 +66,10 @@ describe('health-score', () => { mocks._grepReturns = ['']; mocks._coverageReturns = 0; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.equal(mocks.grep.mock.callCount(), 1); - const [core, exts, includes, excludes] = + const [core, _childProcess, _fs, exts, includes, excludes] = mocks.grep.mock.calls[0].arguments; assert.equal(core, mocks.core); assert.deepEqual(exts, ['js']); @@ -92,10 +92,10 @@ describe('health-score', () => { mocks._grepReturns = ['']; mocks._coverageReturns = 0; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.equal(mocks.grep.mock.callCount(), 1); - const [_core, exts, includes, excludes] = + const [_core, _childProcess, _fs, exts, includes, excludes] = mocks.grep.mock.calls[0].arguments; assert.deepEqual(exts, ['js', 'ts']); assert.deepEqual(includes, ['src', 'lib']); @@ -114,10 +114,10 @@ describe('health-score', () => { mocks._grepReturns = ['']; mocks._coverageReturns = 0; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.equal(mocks.grep.mock.callCount(), 1); - const [_core, exts, includes, excludes] = + const [_core, _childProcess, _fs, exts, includes, excludes] = mocks.grep.mock.calls[0].arguments; assert.deepEqual(exts, ['js', 'ts']); assert.deepEqual(includes, ['src', 'lib']); @@ -136,10 +136,10 @@ describe('health-score', () => { mocks._grepReturns = ['']; mocks._coverageReturns = 0; - await hs.compile(mocks.core, mocks.github); + await hs.compile(mocks.core, mocks.github, mocks.deps); assert.equal(mocks.grep.mock.callCount(), 1); - const [_core, exts, includes, excludes] = + const [_core, _childProcess, _fs, exts, includes, excludes] = mocks.grep.mock.calls[0].arguments; assert.deepEqual(exts, ['js', 'ts']); assert.deepEqual(includes, ['src', 'lib']); diff --git a/test/score_components/coverage-test.js b/test/score_components/coverage-test.js index f526c9c..75d76e7 100644 --- a/test/score_components/coverage-test.js +++ b/test/score_components/coverage-test.js @@ -1,14 +1,14 @@ import assert from 'node:assert'; import { beforeEach, describe, it, mock } from 'node:test'; -import esmock from 'esmock'; +import cov from '../../src/score_components/coverage.js'; describe('score component: code coverage', () => { - let cov; let mockCodecov; let mockCore; let mockGithub; + let mockGetSHA; - beforeEach(async () => { + beforeEach(() => { mockCodecov = { auth: mock.fn(), repos_commits_retrieve: mock.fn(), @@ -30,18 +30,16 @@ describe('score component: code coverage', () => { }, }; - cov = await esmock('../../src/score_components/coverage.js', { - '@api/codecov': { default: mockCodecov }, - }); + mockGetSHA = mock.fn(() => 'abcd1234'); }); it('should export a function', () => { - assert.equal(typeof cov.default, 'function'); + assert.equal(typeof cov, 'function'); }); it('should return 0 if no `codecov_token` input provided', async () => { mockCore.getInput.mock.mockImplementation(() => ''); - const res = await cov.default(mockCore, mockGithub); + const res = await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); assert.equal(res, 0); }); @@ -59,7 +57,7 @@ describe('score component: code coverage', () => { }, }), ); - const res = await cov.default(mockCore, mockGithub); + const res = await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); assert.equal(res, 1337); }); @@ -81,7 +79,7 @@ describe('score component: code coverage', () => { } return Promise.resolve({ data: {} }); }); - const res = await cov.default(mockCore, mockGithub); + const res = await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); assert.strictEqual(res, 42); }); @@ -104,7 +102,7 @@ describe('score component: code coverage', () => { }); const startTime = performance.now(); - const res = await cov.default(mockCore, mockGithub); + const res = await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); const endTime = performance.now(); assert.equal(res, 42); assert.equal(mockCodecov.repos_commits_retrieve.mock.callCount(), 2); @@ -137,7 +135,7 @@ describe('score component: code coverage', () => { return Promise.resolve({ data: { totals: { misses: 42 } } }); }); - const res = await cov.default(mockCore, mockGithub); + const res = await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); assert.equal(res, 0); assert.equal(mockCodecov.repos_commits_retrieve.mock.callCount(), 1); assert.ok( @@ -161,7 +159,7 @@ describe('score component: code coverage', () => { Promise.resolve({ data: {} }), ); - await cov.default(mockCore, mockGithub); + await cov(mockCore, mockGithub, mockCodecov, mockGetSHA); assert.ok( mockCore.error.mock.calls.some((c) => c.arguments[0].includes('Reached maximum attempts'), diff --git a/test/score_components/problematic-comments-test.js b/test/score_components/problematic-comments-test.js index 30297df..70618fd 100644 --- a/test/score_components/problematic-comments-test.js +++ b/test/score_components/problematic-comments-test.js @@ -1,16 +1,15 @@ import assert from 'node:assert'; import { beforeEach, describe, it, mock } from 'node:test'; -import esmock from 'esmock'; +import grepForProblematicComments from '../../src/score_components/find-problematic-comments.js'; const commentPattern = '\\s*(//|/\\*|\\*).*\\b(TODO|HACK|FIXME)\\b'; describe('score component: problematic comments', () => { - let grepForProblematicComments; let mockChildProcess; let mockFs; let mockCore; - beforeEach(async () => { + beforeEach(() => { mockChildProcess = { execSync: mock.fn(), }; @@ -27,15 +26,6 @@ describe('score component: problematic comments', () => { info: mock.fn(), warning: mock.fn(), }; - - const module = await esmock( - '../../src/score_components/find-problematic-comments.js', - { - 'node:child_process': mockChildProcess, - 'node:fs': mockFs, - }, - ); - grepForProblematicComments = module.default; }); it('should export a function', () => { @@ -48,7 +38,14 @@ describe('score component: problematic comments', () => { }); mockFs.existsSync.mock.mockImplementation(() => false); const score = { - comments: grepForProblematicComments(mockCore, ['js'], [], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + [], + [], + ), coverageMisses: 0, }; assert.ok( @@ -66,7 +63,14 @@ describe('score component: problematic comments', () => { mockChildProcess.execSync.mock.mockImplementation(() => ''); mockFs.existsSync.mock.mockImplementation(() => false); const score = { - comments: grepForProblematicComments(mockCore, ['js'], [], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + [], + [], + ), coverageMisses: 0, }; let find = 'find .'; @@ -79,7 +83,14 @@ describe('score component: problematic comments', () => { it('should default to gitignore if excludes not provided', () => { mockChildProcess.execSync.mock.mockImplementation(() => ''); mockFs.existsSync.mock.mockImplementation(() => false); - grepForProblematicComments(mockCore, ['js'], [], []); + grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + [], + [], + ); assert.equal(mockFs.existsSync.mock.calls[0].arguments[0], '.gitignore'); }); @@ -89,6 +100,8 @@ describe('score component: problematic comments', () => { const score = { comments: grepForProblematicComments( mockCore, + mockChildProcess, + mockFs, ['js'], ['src'], ['test/'], @@ -109,6 +122,8 @@ describe('score component: problematic comments', () => { const score = { comments: grepForProblematicComments( mockCore, + mockChildProcess, + mockFs, ['js', 'ts'], ['src', 'dir'], ['test/', 'dist'], @@ -128,7 +143,14 @@ describe('score component: problematic comments', () => { it('should handle empty grep results', () => { mockChildProcess.execSync.mock.mockImplementation(() => ''); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, []); @@ -139,7 +161,14 @@ describe('score component: problematic comments', () => { () => 'path:10: // TODO: random stuff', ); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, [ @@ -158,7 +187,14 @@ describe('score component: problematic comments', () => { 'path:10: // TODO: random stuff \n path2:15: // FIXME: random stuff', ); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, [ @@ -183,7 +219,14 @@ describe('score component: problematic comments', () => { 'path:10: // TODO: random stuff \n path2:15: // random things TODO', ); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, [ @@ -207,7 +250,14 @@ describe('score component: problematic comments', () => { () => 'path:10: // TODO random stuff', ); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, [ @@ -225,7 +275,14 @@ describe('score component: problematic comments', () => { () => 'path:10: const result = "hello:world" // TODO: random stuff', ); const score = { - comments: grepForProblematicComments(mockCore, ['js'], ['src'], []), + comments: grepForProblematicComments( + mockCore, + mockChildProcess, + mockFs, + ['js'], + ['src'], + [], + ), coverageMisses: 0, }; assert.deepEqual(score.comments, [ diff --git a/test/stubs/stubs.js b/test/stubs/stubs.js index 2796860..f685cdd 100644 --- a/test/stubs/stubs.js +++ b/test/stubs/stubs.js @@ -58,6 +58,13 @@ export class Mock { this._grepReturns = []; this._coverageReturns = 0; + this.deps = { + codecov: this.codecov, + getSHA: mock.fn(() => 'abc123'), + childProcess: this.childProcess, + fs: this.fs, + }; + // Create method mocks for hs module this.grep = mock.method(hs, 'grep', () => this._grepReturns); this.coverage = mock.method(hs, 'coverage', () =>