diff --git a/.gitignore b/.gitignore index 62ab16d..8bb8471 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ package-lock.json .history/ .vscode/ .link/ +.kiro/ diff --git a/dtc/package.json b/dtc/package.json index fd8b90d..56161ab 100644 --- a/dtc/package.json +++ b/dtc/package.json @@ -21,7 +21,6 @@ "@cgauge/assert": "^0.27.0", "@cgauge/log": "^0.27.0", "@cgauge/type-guard": "^0.27.0", - "cleye": "^1.3.2", "nock": "^14.0.1" }, "scripts": { diff --git a/dtc/src/args.ts b/dtc/src/args.ts new file mode 100644 index 0000000..ecc9625 --- /dev/null +++ b/dtc/src/args.ts @@ -0,0 +1,53 @@ +import {parseArgs} from 'node:util' + +export type ParsedCliArgs = { + configPath: string | undefined + filePath: string | undefined + runnerArgs: string[] + help: boolean +} + +export const parseCliArgs = (argv: string[]): ParsedCliArgs => { + const {values, positionals, tokens} = parseArgs({ + args: argv, + options: { + config: {type: 'string', short: 'c'}, + help: {type: 'boolean', short: 'h', default: false}, + }, + strict: true, + allowPositionals: true, + tokens: true, + }) + + const separatorIndex = tokens.findIndex((t) => t.kind === 'option-terminator') + let filePath: string | undefined + let runnerArgs: string[] = [] + + if (separatorIndex !== -1) { + const separatorToken = tokens[separatorIndex] + filePath = + positionals.length > 0 && tokens.some((t) => t.kind === 'positional' && t.index < separatorToken.index) + ? positionals[0] + : undefined + runnerArgs = argv.slice(separatorToken.index + 1) + } else { + filePath = positionals[0] + } + + return { + configPath: values.config, + filePath, + runnerArgs, + help: values.help as boolean, + } +} + +const HELP_TEXT = `Usage: dtc [filePath] [--config ] [-- runnerArgs...] + +Options: + -c, --config Configuration file path + -h, --help Show this help message` + +export const printHelp = (): void => { + console.log(HELP_TEXT) +} diff --git a/dtc/src/cli.ts b/dtc/src/cli.ts index f37a9e0..4fcfade 100755 --- a/dtc/src/cli.ts +++ b/dtc/src/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import {cli} from 'cleye' +import {parseCliArgs, printHelp} from './args.js' import {resolveConfig} from './config.js' import {loadTestCases} from './loader.js' import {error, warnExit} from './utils.js' @@ -27,24 +27,19 @@ const main = async ({projectPath, configPath, filePath, runnerArgs}: CliArgs) => await config.runner(testCaseExecutions, config.plugins, runnerArgs, configPath, filePath) } -const argv = cli({ - name: 'dtc', - parameters: ['[filePath]', '--', '[runnerArgs...]'], - flags: { - config: { - alias: 'c', - type: String, - description: 'Configuration file path', - }, - }, -}) - try { + const parsed = parseCliArgs(process.argv.slice(2)) + + if (parsed.help) { + printHelp() + process.exit(0) + } + await main({ projectPath: process.cwd(), - configPath: argv.flags.config, - filePath: argv._.filePath, - runnerArgs: argv._.runnerArgs, + configPath: parsed.configPath, + filePath: parsed.filePath, + runnerArgs: parsed.runnerArgs, }) } catch (e: any) { error(`${e.code}: ${e.message}`) diff --git a/dtc/test/args.test.ts b/dtc/test/args.test.ts new file mode 100644 index 0000000..7314997 --- /dev/null +++ b/dtc/test/args.test.ts @@ -0,0 +1,78 @@ +import {test} from 'node:test' +import assert from 'node:assert' +import {parseCliArgs} from '../src/args.js' + +test('No arguments returns all undefined/empty defaults', () => { + const result = parseCliArgs([]) + + assert.deepStrictEqual(result, { + configPath: undefined, + filePath: undefined, + runnerArgs: [], + help: false, + }) +}) + +test('--config flag returns configPath', () => { + const result = parseCliArgs(['--config', 'path.js']) + + assert.strictEqual(result.configPath, 'path.js') +}) + +test('-c short alias returns configPath', () => { + const result = parseCliArgs(['-c', 'path.js']) + + assert.strictEqual(result.configPath, 'path.js') +}) + +test('Positional argument returns filePath', () => { + const result = parseCliArgs(['file.ts']) + + assert.strictEqual(result.filePath, 'file.ts') +}) + +test('Positional and --config returns both independently', () => { + const result = parseCliArgs(['file.ts', '--config', 'c.js']) + + assert.strictEqual(result.filePath, 'file.ts') + assert.strictEqual(result.configPath, 'c.js') +}) + +test('-- separator collects runner args', () => { + const result = parseCliArgs(['--', 'a', 'b']) + + assert.deepStrictEqual(result.runnerArgs, ['a', 'b']) +}) + +test('filePath and -- separator returns both correctly', () => { + const result = parseCliArgs(['file.ts', '--', 'a']) + + assert.strictEqual(result.filePath, 'file.ts') + assert.deepStrictEqual(result.runnerArgs, ['a']) +}) + +test('--help returns help: true', () => { + const result = parseCliArgs(['--help']) + + assert.strictEqual(result.help, true) +}) + +test('-h returns help: true', () => { + const result = parseCliArgs(['-h']) + + assert.strictEqual(result.help, true) +}) + +test('--config without value throws error', () => { + assert.throws(() => parseCliArgs(['--config'])) +}) + +test('--unknown flag throws error containing the flag name', () => { + assert.throws( + () => parseCliArgs(['--unknown']), + (err: Error) => { + assert.ok(err.message.includes('unknown'), `Expected error message to include "unknown", got: ${err.message}`) + return true + }, + ) +}) diff --git a/dtc/test/cli.test.ts b/dtc/test/cli.test.ts index 9bfc0c2..2f3d79f 100644 --- a/dtc/test/cli.test.ts +++ b/dtc/test/cli.test.ts @@ -1,13 +1,14 @@ import {test} from 'node:test' import {spawnSync} from 'node:child_process' import nodeAssert from 'node:assert' -import { dirname } from 'node:path' +import {dirname} from 'node:path' import {fileURLToPath} from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) +const cliPath = `${__dirname}/../src/cli.ts` test('It executes test using cli', async () => { - const childProcess = spawnSync('npx', ['tsx', `${__dirname}/../src/cli.ts`, './test/fixtures/unit.js'], { + const childProcess = spawnSync('npx', ['tsx', cliPath, './test/fixtures/unit.js'], { stdio: 'inherit', }) @@ -15,13 +16,48 @@ test('It executes test using cli', async () => { }) test('It fails test using cli', async () => { - const childProcess = spawnSync('npx', ['tsx', `${__dirname}/../src/cli.ts`, './test/fixtures/unit-fail.js']) + const childProcess = spawnSync('npx', ['tsx', cliPath, './test/fixtures/unit-fail.js']) nodeAssert.equal(childProcess.status, 1) }) test('It executes all files', async () => { - const childProcess = spawnSync('npx', ['tsx', `${__dirname}/../src/cli.ts`], {stdio: 'inherit'}) + const childProcess = spawnSync('npx', ['tsx', cliPath], {stdio: 'inherit'}) nodeAssert.equal(childProcess.status, 0) }) + +test('dtc --help outputs usage info to stdout and exits with code 0', async () => { + const childProcess = spawnSync('npx', ['tsx', cliPath, '--help'], {encoding: 'utf-8'}) + + nodeAssert.equal(childProcess.status, 0) + nodeAssert.ok(childProcess.stdout.includes('Usage: dtc'), 'stdout should contain usage info') + nodeAssert.ok(childProcess.stdout.includes('--config'), 'stdout should mention --config flag') + nodeAssert.ok(childProcess.stdout.includes('--help'), 'stdout should mention --help flag') +}) + +test('dtc -h outputs usage info to stdout and exits with code 0', async () => { + const childProcess = spawnSync('npx', ['tsx', cliPath, '-h'], {encoding: 'utf-8'}) + + nodeAssert.equal(childProcess.status, 0) + nodeAssert.ok(childProcess.stdout.includes('Usage: dtc'), 'stdout should contain usage info') + nodeAssert.ok(childProcess.stdout.includes('--config'), 'stdout should mention --config flag') + nodeAssert.ok(childProcess.stdout.includes('--help'), 'stdout should mention --help flag') +}) + +test('dtc --unknown writes error to stderr, no stdout output, exits with code 1', async () => { + const childProcess = spawnSync('npx', ['tsx', cliPath, '--unknown'], {encoding: 'utf-8'}) + + nodeAssert.equal(childProcess.status, 1) + nodeAssert.ok(childProcess.stderr.length > 0, 'stderr should contain error message') + nodeAssert.ok(childProcess.stderr.includes('unknown'), 'stderr should mention the unknown flag') + nodeAssert.equal(childProcess.stdout, '', 'stdout should be empty') +}) + +test('dtc --config without value writes error to stderr and exits with code 1', async () => { + const childProcess = spawnSync('npx', ['tsx', cliPath, '--config'], {encoding: 'utf-8'}) + + nodeAssert.equal(childProcess.status, 1) + nodeAssert.ok(childProcess.stderr.length > 0, 'stderr should contain error message') + nodeAssert.ok(childProcess.stderr.includes('config'), 'stderr should mention the config flag') +}) diff --git a/log/src/index.ts b/log/src/index.ts index 896e7f8..ad8355a 100644 --- a/log/src/index.ts +++ b/log/src/index.ts @@ -16,9 +16,15 @@ const createLogFunction = (namespace: string, options: InspectOptions) => (level }) } +const createLogErrorFunction = (namespace: string, options: InspectOptions) => (level: LOG_LEVEL) => (...messages: unknown[]) => { + messages.forEach((v) => { + console.error(`${namespace.toLocaleUpperCase()} ${styleText(['blue'], level.toUpperCase())} ${inspect(v, options)}`) + }) +} + export default (namespace: string, options: InspectOptions = {depth: 20, colors: true}) => ({ debug: createLogFunction(namespace, options)('debug'), info: createLogFunction(namespace, options)('info'), warn: createLogFunction(namespace, options)('warning'), - error: createLogFunction(namespace, options)('error'), + error: createLogErrorFunction(namespace, options)('error'), })