From 4f14f20ec207c4b80442433ae9c0e9f1137f7d4f Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Sat, 8 Apr 2023 01:27:57 -0400 Subject: [PATCH] feat: set worker count from cli (#14) * feat: basic worker setting support * ci: enable in CI * refactor: split off argument parser * feat: generate helptext from flag configuration --- .github/workflows/nodejs.yml | 2 +- src/argumentParser.ts | 69 ++++++++++++++++++++++++++++++++++++ src/help.ts | 25 +++++++++++-- src/runner.ts | 38 +++----------------- src/types.ts | 11 ++++++ 5 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 src/argumentParser.ts diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 97093ed..f0f31e1 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -70,7 +70,7 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }} - run: | yarn - yarn test + yarn test --workers=2 build: runs-on: ubuntu-latest diff --git a/src/argumentParser.ts b/src/argumentParser.ts new file mode 100644 index 0000000..2515067 --- /dev/null +++ b/src/argumentParser.ts @@ -0,0 +1,69 @@ +import { type Args, type FlagConfigurationMap } from './types' + +export const FLAG_CONFIGURATION: Readonly = { + workers: { + requiresValue: true, + default: 1, + description: 'Defines up to how many parallel processes should consume tests.', + }, + help: { + requiresValue: false, + default: false, + description: 'Displays the help text.', + }, +} + +class MalformedArgumentError extends Error {} + +function parseFlags(flags: Array): Map { + const flagMap = new Map() + + for (const flag of flags) { + const [flagName, flagValue] = flag.split('=') as Array + const flagNameWithoutPrefix = flagName.replace(/^--/, '') + const flagConfiguration = FLAG_CONFIGURATION[flagNameWithoutPrefix] + + if (!flagConfiguration) throw new MalformedArgumentError(`"${flagName}" is not a valid flag.`) + + if (flagConfiguration.requiresValue && !flagValue) + throw new MalformedArgumentError(`"${flagName}" requires a value.`) + + flagMap.set(flagNameWithoutPrefix, flagValue ?? true) + } + + return flagMap +} + +function parseArgs(args: Array): Args { + const [, runtimePath, ...userArgs] = args + + const { + argsWithoutFlags, + flags, + }: { + argsWithoutFlags: Array + flags: Array + } = (userArgs as Array).reduce( + (acc, arg: string) => { + if (arg.startsWith('--')) acc.flags.push(arg) + else acc.argsWithoutFlags.push(arg) + + return acc + }, + { argsWithoutFlags: [], flags: [] } as { + argsWithoutFlags: Array + flags: Array + }, + ) + + const parsedFlags = parseFlags(flags) + + return { + runtimePath, + targets: argsWithoutFlags ?? [], + help: Boolean(parsedFlags.get('help') ?? FLAG_CONFIGURATION['help'].default), + workers: Number(parsedFlags.get('workers') ?? FLAG_CONFIGURATION['workers'].default), + } +} + +export default parseArgs diff --git a/src/help.ts b/src/help.ts index c51716b..85a9aec 100644 --- a/src/help.ts +++ b/src/help.ts @@ -1,12 +1,31 @@ import { boldText } from './utils' +import { FLAG_CONFIGURATION } from './argumentParser' + +function getFlagHelp(): string { + const lines: Array = [] + + for (const flag of Object.keys(FLAG_CONFIGURATION)) { + const requiresValue = FLAG_CONFIGURATION[flag].requiresValue + lines.push( + [ + `--${flag}${requiresValue ? '=' : ''}`, + `${FLAG_CONFIGURATION[flag].description} (default=${FLAG_CONFIGURATION[flag].default})`, + ] + .map((segment) => segment.padEnd(15)) + .join(''), + ) + } + + return lines.join('\n') +} export default ` ${boldText('Works on my machine v0.0.0')} A no-dependency test runner --- -womm [--help] [-h] ... +womm ... -Flags: ---help, -h: Prints this message +${boldText('Flags:')} +${getFlagHelp()} ` diff --git a/src/runner.ts b/src/runner.ts index e3cdfde..55aae28 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -3,12 +3,15 @@ import { getContext, greenText, redText, exec, splitIntoBatches } from './utils' import helpText from './help' import { type Args, type IContext, type TestServer } from './types' +import parseArgs from './argumentParser' import { type Buffer } from 'buffer' import { promises as fs } from 'fs' import path from 'path' import net from 'net' +class UnknownArgumentError extends Error {} + /* * Collects test files recursively starting from the provided root * path. @@ -90,39 +93,6 @@ function setUpSocket(socketPath: string): TestServer { }) return server -} - -function parseArgs(args: Array): Args { - const [, runtimePath, ...userArgs] = args - - const { - argsWithoutFlags, - shortFlags, - longFlags, - }: { - argsWithoutFlags: Array - longFlags: Array - shortFlags: Array - } = (userArgs as Array).reduce( - (acc, arg: string) => { - if (arg.startsWith('--')) acc.longFlags.push(arg) - else if (arg.startsWith('-')) acc.shortFlags.push(arg) - else acc.argsWithoutFlags.push(arg) - - return acc - }, - { argsWithoutFlags: [], longFlags: [], shortFlags: [] } as { - argsWithoutFlags: Array - longFlags: Array - shortFlags: Array - }, - ) - - return { - runtimePath, - targets: argsWithoutFlags, - help: longFlags.includes('--help') || shortFlags.includes('-h'), - } } /* * Logic executed when running the test runner CLI. */ @@ -142,7 +112,7 @@ function parseArgs(args: Array): Args { const collectedTests = await collectTests(args.targets) await collectCases(context, collectedTests) - await assignTestsToWorkers(context, collectedTests) + await assignTestsToWorkers(context, collectedTests, args.workers) if (server.failure) throw new Error() } catch (e) { diff --git a/src/types.ts b/src/types.ts index d6e499f..574ce42 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,7 @@ export interface Args { targets: Array runtimePath: string help: boolean + workers: number } export interface MatcherReport { @@ -45,3 +46,13 @@ export interface RawMatchersMap { comparisonMatchers: Array noArgMatchers: Array } + +interface FlagConfiguration { + requiresValue: boolean + default: string | boolean | number + description: string +} + +export interface FlagConfigurationMap { + [key: string]: FlagConfiguration +}