diff --git a/changelog.md b/changelog.md index ea29d1d..d8f6b52 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-05-23 - 1.10.0 - feat(cli) +Add --startFrom and --stopAt options to filter test files by range + +- Introduced CLI options --startFrom and --stopAt in ts/index.ts for selective test execution +- Added validation to ensure provided range values are positive and startFrom is not greater than stopAt +- Propagated file range filtering into test grouping in tstest.classes.tstest.ts, applying the range filter across serial and parallel groups +- Updated usage messages to include the new options + ## 2025-05-23 - 1.9.4 - fix(docs) Update documentation and configuration for legal notices and CI permissions. This commit adds a new local settings file for tool permissions, refines the legal and trademark sections in the readme, and improves glob test files with clearer log messages. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 05a4204..d1459a9 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tstest', - version: '1.9.4', + version: '1.10.0', description: 'a test utility to run tests that match test/**/*.ts' } diff --git a/ts/index.ts b/ts/index.ts index fbe65f7..7d2f5f4 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -13,6 +13,8 @@ export const runCli = async () => { const logOptions: LogOptions = {}; let testPath: string | null = null; let tags: string[] = []; + let startFromFile: number | null = null; + let stopAtFile: number | null = null; // Parse options for (let i = 0; i < args.length; i++) { @@ -42,6 +44,32 @@ export const runCli = async () => { tags = args[++i].split(','); } break; + case '--startFrom': + if (i + 1 < args.length) { + const value = parseInt(args[++i], 10); + if (isNaN(value) || value < 1) { + console.error('Error: --startFrom must be a positive integer'); + process.exit(1); + } + startFromFile = value; + } else { + console.error('Error: --startFrom requires a number argument'); + process.exit(1); + } + break; + case '--stopAt': + if (i + 1 < args.length) { + const value = parseInt(args[++i], 10); + if (isNaN(value) || value < 1) { + console.error('Error: --stopAt must be a positive integer'); + process.exit(1); + } + stopAtFile = value; + } else { + console.error('Error: --stopAt requires a number argument'); + process.exit(1); + } + break; default: if (!arg.startsWith('-')) { testPath = arg; @@ -49,16 +77,24 @@ export const runCli = async () => { } } + // Validate test file range options + if (startFromFile !== null && stopAtFile !== null && startFromFile > stopAtFile) { + console.error('Error: --startFrom cannot be greater than --stopAt'); + process.exit(1); + } + if (!testPath) { console.error('You must specify a test directory/file/pattern as argument. Please try again.'); console.error('\nUsage: tstest [options]'); console.error('\nOptions:'); - console.error(' --quiet, -q Minimal output'); - console.error(' --verbose, -v Verbose output'); - console.error(' --no-color Disable colored output'); - console.error(' --json Output results as JSON'); - console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log'); - console.error(' --tags Run only tests with specified tags (comma-separated)'); + console.error(' --quiet, -q Minimal output'); + console.error(' --verbose, -v Verbose output'); + console.error(' --no-color Disable colored output'); + console.error(' --json Output results as JSON'); + console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log'); + console.error(' --tags Run only tests with specified tags (comma-separated)'); + console.error(' --startFrom Start running from test file number n'); + console.error(' --stopAt Stop running at test file number n'); process.exit(1); } @@ -73,6 +109,11 @@ export const runCli = async () => { executionMode = TestExecutionMode.DIRECTORY; } - const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags); + const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile); await tsTestInstance.run(); }; + +// Execute CLI when this file is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + runCli(); +} diff --git a/ts/tstest.classes.tstest.ts b/ts/tstest.classes.tstest.ts index 9ed8a94..b0221e6 100644 --- a/ts/tstest.classes.tstest.ts +++ b/ts/tstest.classes.tstest.ts @@ -16,6 +16,8 @@ export class TsTest { public executionMode: TestExecutionMode; public logger: TsTestLogger; public filterTags: string[]; + public startFromFile: number | null; + public stopAtFile: number | null; public smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', @@ -26,16 +28,35 @@ export class TsTest { public tsbundleInstance = new plugins.tsbundle.TsBundle(); - constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = []) { + constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null) { this.executionMode = executionModeArg; this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg); this.logger = new TsTestLogger(logOptions); this.filterTags = tags; + this.startFromFile = startFromFile; + this.stopAtFile = stopAtFile; } async run() { const testGroups = await this.testDir.getTestFileGroups(); - const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()]; + let allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()]; + + // Apply file range filtering if specified + if (this.startFromFile !== null || this.stopAtFile !== null) { + const startIndex = this.startFromFile ? this.startFromFile - 1 : 0; // Convert to 0-based index + const endIndex = this.stopAtFile ? this.stopAtFile : allFiles.length; + allFiles = allFiles.slice(startIndex, endIndex); + + // Filter the serial and parallel groups based on remaining files + testGroups.serial = testGroups.serial.filter(file => allFiles.includes(file)); + Object.keys(testGroups.parallelGroups).forEach(groupName => { + testGroups.parallelGroups[groupName] = testGroups.parallelGroups[groupName].filter(file => allFiles.includes(file)); + // Remove empty groups + if (testGroups.parallelGroups[groupName].length === 0) { + delete testGroups.parallelGroups[groupName]; + } + }); + } // Log test discovery this.logger.testDiscovery(