From 7255dbd52e02276784d7bf0369cf9be0553058d0 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 26 Dec 2024 16:40:51 +0800 Subject: [PATCH] refactor cov --- package.json | 5 +- src/baseCommand.ts | 21 ++++++-- src/commands/cov.ts | 104 +++++++++++++++++++++++++++++++------- src/commands/test.ts | 10 ++-- src/hooks/init/options.ts | 4 +- test/cmd/cov.test.ts | 23 +++++++-- test/egg-bin.test.ts | 22 ++++---- 7 files changed, 146 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 066cd117..6202108a 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,9 @@ "topicSeparator": " ", "hooks": { "init": "./dist/esm/hooks/init/options" - } + }, + "additionalHelpFlags": [ + "-h" + ] } } diff --git a/src/baseCommand.ts b/src/baseCommand.ts index 837eef99..00498be3 100644 --- a/src/baseCommand.ts +++ b/src/baseCommand.ts @@ -1,5 +1,4 @@ import { debuglog } from 'node:util'; -// import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { fork, ForkOptions, ChildProcess } from 'node:child_process'; import { Command, Flags, Interfaces } from '@oclif/core'; @@ -92,13 +91,18 @@ export abstract class BaseCommand extends Command { helpGroup: 'GLOBAL', summary: 'TypeScript compiler, like ts-node/register', }), - // flag with no value (--ts, --typescript) + // flag with no value (--typescript) typescript: Flags.boolean({ helpGroup: 'GLOBAL', description: '[default: true] use TypeScript to run the test', aliases: [ 'ts' ], allowNo: true, }), + ts: Flags.string({ + helpGroup: 'GLOBAL', + description: 'shortcut for --typescript, e.g.: --ts=false', + options: [ 'true', 'false' ], + }), javascript: Flags.boolean({ helpGroup: 'GLOBAL', description: 'use JavaScript to run the test', @@ -131,6 +135,7 @@ export abstract class BaseCommand extends Command { public async init(): Promise { await super.init(); + debug('raw args: %o', this.argv); const { args, flags } = await this.parse({ flags: this.ctor.flags, baseFlags: (super.ctor as typeof BaseCommand).baseFlags, @@ -155,7 +160,15 @@ export abstract class BaseCommand extends Command { this.pkg = pkg; this.pkgEgg = pkg.egg ?? {}; flags.tscompiler = flags.tscompiler ?? this.env.TS_COMPILER ?? this.pkgEgg.tscompiler; - let typescript = args.typescript; + + let typescript: boolean = args.typescript; + // keep compatible with old ts flag: `--ts=true` or `--ts=false` + if (flags.ts === 'true') { + typescript = true; + } else if (flags.ts === 'false') { + typescript = false; + } + if (typescript === undefined) { // try to ready EGG_TYPESCRIPT env first, only accept 'true' or 'false' string if (this.env.EGG_TYPESCRIPT === 'false') { @@ -336,7 +349,7 @@ export abstract class BaseCommand extends Command { proc.pid, NODE_OPTIONS, process.execPath, - modulePath, forkArgs.map(a => `${a}`).join(' ')); + modulePath, forkArgs.map(a => `'${a}'`).join(' ')); graceful(proc); return new Promise((resolve, reject) => { diff --git a/src/commands/cov.ts b/src/commands/cov.ts index 365c1790..9205787a 100644 --- a/src/commands/cov.ts +++ b/src/commands/cov.ts @@ -1,30 +1,100 @@ -import { Args, Command, Flags } from '@oclif/core'; +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { Flags } from '@oclif/core'; +import Test from './test.js'; +import { importResolve } from '@eggjs/utils'; +import { ForkNodeOptions } from '../baseCommand.js'; -export default class Cov extends Command { - static override args = { - file: Args.string({ description: 'file to read' }), - }; - - static override description = 'describe the command here'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export default class Cov extends Test { + static override description = 'Run the test with coverage'; static override examples = [ '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> test/index.test.ts', ]; static override flags = { - // flag with no value (-f, --force) - force: Flags.boolean({ char: 'f' }), - // flag with a value (-n, --name=VALUE) - name: Flags.string({ char: 'n', description: 'name to print' }), + ...Test.flags, + // will use on egg-mock https://github.com/eggjs/egg-mock/blob/84a64bd19d0569ec94664c898fb1b28367b95d60/index.js#L7 + prerequire: Flags.boolean({ + description: 'prerequire files for coverage instrument', + }), + exclude: Flags.string({ + description: 'coverage ignore, one or more files patterns`', + multiple: true, + char: 'x', + }), + c8: Flags.string({ + description: 'c8 instruments passthrough`', + default: '--temp-directory node_modules/.c8_output -r text-summary -r json-summary -r json -r lcov -r cobertura', + }), }; - public async run(): Promise { - const { args, flags } = await this.parse(Cov); + protected get defaultExcludes() { + return [ + 'example/', + 'examples/', + 'mocks**/', + 'docs/', + // https://github.com/JaKXz/test-exclude/blob/620a7be412d4fc2070d50f0f63e3228314066fc9/index.js#L73 + 'test/**', + 'test{,-*}.js', + '**/*.test.js', + '**/__tests__/**', + '**/node_modules/**', + 'typings', + '**/*.d.ts', + ]; + } + + protected override async forkNode(modulePath: string, forkArgs: string[], options: ForkNodeOptions = {}) { + const { flags } = this; + if (flags.prerequire) { + this.env.EGG_BIN_PREREQUIRE = 'true'; + } - const name = flags.name ?? 'world'; - this.log(`hello ${name} from /Users/fengmk2/git/github.com/eggjs/bin/src/commands/cov.ts`); - if (args.file && flags.force) { - this.log(`you input --force and --file: ${args.file}`); + // add c8 args + // https://github.com/eggjs/egg/issues/3930 + const c8Args = [ + // '--show-process-tree', + ...flags.c8.split(' ').filter(a => a.trim()), + ]; + if (flags.typescript) { + this.env.SPAWN_WRAP_SHIM_ROOT = path.join(flags.base, 'node_modules'); + c8Args.push('--extension'); + c8Args.push('.ts'); } + + const excludes = new Set([ + ...process.env.COV_EXCLUDES?.split(',') ?? [], + ...this.defaultExcludes, + ...Array.from(flags.exclude ?? []), + ]); + for (const exclude of excludes) { + c8Args.push('-x'); + c8Args.push(exclude); + } + const c8File = importResolve('c8/bin/c8.js'); + const outputDir = path.join(flags.base, 'node_modules/.c8_output'); + await fs.rm(outputDir, { force: true, recursive: true }); + const coverageDir = path.join(flags.base, 'coverage'); + await fs.rm(coverageDir, { force: true, recursive: true }); + + const execArgv = [ + ...this.globalExecArgv, + ...options.execArgv || [], + ]; + this.globalExecArgv = []; + + // $ c8 node mocha + await super.forkNode(c8File, [ + ...c8Args, + process.execPath, + ...execArgv, + modulePath, + ...forkArgs, + ]); } } diff --git a/src/commands/test.ts b/src/commands/test.ts index 13e0e087..4b5b9dfd 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -9,7 +9,7 @@ import { BaseCommand } from '../baseCommand.js'; const debug = debuglog('@eggjs/bin/commands/test'); -export default class Test extends BaseCommand { +export default class Test extends BaseCommand { static override args = { file: Args.string({ description: 'file(s) to test', @@ -93,6 +93,10 @@ export default class Test extends BaseCommand { const mochaArgs = await this.formatMochaArgs(); if (!mochaArgs) return; + await this.runMocha(mochaFile, mochaArgs); + } + + protected async runMocha(mochaFile: string, mochaArgs: string[]) { await this.forkNode(mochaFile, mochaArgs, { execArgv: [ ...process.execArgv, @@ -156,7 +160,7 @@ export default class Test extends BaseCommand { files.sort(); if (files.length === 0) { - console.log(`No test files found with ${pattern}`); + console.log('No test files found with pattern %o', pattern); return; } @@ -172,7 +176,6 @@ export default class Test extends BaseCommand { const grep = flags.grep ? flags.grep.split(',') : []; return [ - flags['dry-run'] ? '--dry-run' : '', // force exit '--exit', flags.bail ? '--bail' : '', @@ -184,6 +187,7 @@ export default class Test extends BaseCommand { reporterOptions ? `--reporter-options=${reporterOptions}` : '', ...requires.map(r => `--require=${r}`), ...files, + flags['dry-run'] ? '--dry-run' : '', ].filter(a => a.trim()); } } diff --git a/src/hooks/init/options.ts b/src/hooks/init/options.ts index 6006ea27..243ad4de 100644 --- a/src/hooks/init/options.ts +++ b/src/hooks/init/options.ts @@ -1,7 +1,9 @@ // import { Hook } from '@oclif/core'; // const hook: Hook<'init'> = async function(opts) { -// process.stdout.write(`example hook running ${opts.id}\n`); +// if (opts.argv.includes('-h')) { +// this.config.runCommand('help'); +// } // }; // export default hook; diff --git a/test/cmd/cov.test.ts b/test/cmd/cov.test.ts index d82eb054..ef6e5e05 100644 --- a/test/cmd/cov.test.ts +++ b/test/cmd/cov.test.ts @@ -21,7 +21,22 @@ describe('test/cmd/cov.test.ts', () => { } describe('egg-bin cov', () => { - it('should success on js', async () => { + it('should success on js with --javascript', async () => { + await coffee.fork(eggBin, [ 'cov', '--javascript' ], { cwd, env: { TESTS: 'test/**/*.test.js' } }) + // .debug() + .expect('stdout', /should success/) + .expect('stdout', /a\.test\.js/) + .expect('stdout', /b[\/|\\]b\.test\.js/) + .notExpect('stdout', /\ba\.js/) + .expect('stdout', /Statements {3}:/) + .expect('code', 0) + .end(); + assertCoverage(cwd); + const lcov = await fs.readFile(path.join(cwd, 'coverage/lcov.info'), 'utf8'); + assert.match(lcov, /ignore[\/|\\]a.js/); + }); + + it('should success on js with --ts=false', async () => { await coffee.fork(eggBin, [ 'cov', '--ts=false' ], { cwd, env: { TESTS: 'test/**/*.test.js' } }) // .debug() .expect('stdout', /should success/) @@ -162,7 +177,7 @@ describe('test/cmd/cov.test.ts', () => { .end(); }); - it('test parallel', () => { + it.skip('test parallel', () => { if (process.platform === 'win32') return; return coffee.fork(eggBin, [ 'cov', '--parallel', '--ts=false' ], { cwd: getFixtures('test-demo-app'), @@ -180,7 +195,7 @@ describe('test/cmd/cov.test.ts', () => { return coffee.fork(eggBin, [ 'cov' ], { cwd, }) - .debug() + // .debug() .expect('stdout', /should work/) .expect('stdout', /2 passing/) .expect('code', 0) @@ -193,7 +208,7 @@ describe('test/cmd/cov.test.ts', () => { return coffee.fork(eggBin, [ 'cov' ], { cwd: getFixtures('egg-revert'), }) - .debug() + // .debug() .expect('stdout', /SECURITY WARNING: Reverting CVE-2023-46809: Marvin attack on PKCS#1 padding/) .expect('stdout', /1 passing/) .expect('code', 0) diff --git a/test/egg-bin.test.ts b/test/egg-bin.test.ts index 5206c512..10e6958a 100644 --- a/test/egg-bin.test.ts +++ b/test/egg-bin.test.ts @@ -9,7 +9,7 @@ describe('test/egg-bin.test.ts', () => { describe('global options', () => { it('should show version', () => { return coffee.fork(eggBin, [ '--version' ], { cwd }) - .debug() + // .debug() .expect('stdout', /\d+\.\d+\.\d+/) .expect('code', 0) .end(); @@ -18,10 +18,8 @@ describe('test/egg-bin.test.ts', () => { it('should main redirect to help', () => { return coffee.fork(eggBin, [], { cwd }) // .debug() - .expect('stdout', /Usage: egg-bin/) - .expect('stdout', /Available Commands/) - .expect('stdout', /test \[files\.\.\.]\s+Run the test/) - .expect('stdout', /-ts, --typescript\s+whether enable typescript support/) + .expect('stdout', /USAGE/) + .expect('stdout', /\$ egg-bin \[COMMAND]/) .expect('code', 0) .end(); }); @@ -29,10 +27,8 @@ describe('test/egg-bin.test.ts', () => { it('should show help', () => { return coffee.fork(eggBin, [ '--help' ], { cwd }) // .debug() - .expect('stdout', /Usage: egg-bin/) - .expect('stdout', /Available Commands/) - .expect('stdout', /test \[files\.\.\.]\s+Run the test/) - .expect('stdout', /-ts, --typescript\s+whether enable typescript support/) + .expect('stdout', /USAGE/) + .expect('stdout', /\$ egg-bin \[COMMAND]/) .expect('code', 0) .end(); }); @@ -40,8 +36,8 @@ describe('test/egg-bin.test.ts', () => { it('should show egg-bin test help', () => { return coffee.fork(eggBin, [ 'test', '-h', '--base', cwd ]) // .debug() - .expect('stdout', /Usage: egg-bin test \[files\.\.\.]/) - .expect('stdout', /-ts, --typescript\s+whether enable typescript support/) + .expect('stdout', /Run the test/) + .expect('stdout', /--\[no-]typescript {5}\[default: true] use TypeScript to run the test/) .expect('code', 0) .end(); }); @@ -49,8 +45,8 @@ describe('test/egg-bin.test.ts', () => { it('should show help when command not exists', () => { return coffee.fork(eggBin, [ 'not-exists' ], { cwd }) // .debug() - .expect('stderr', /Command is not found: 'egg-bin not-exists', try 'egg-bin --help' for more information/) - .expect('code', 1) + .expect('stderr', /command not-exists not found/) + .expect('code', 2) .end(); }); });