From 4236784aebfa38e7a92c511277faec07707e17f0 Mon Sep 17 00:00:00 2001 From: Sable Date: Thu, 2 Apr 2026 07:43:11 +0200 Subject: [PATCH] feat: support multiple RSpec directories --- README.md | 2 +- package.json | 10 ++++++++-- src/adapter.ts | 23 ++++++++++------------- src/rspecTests.ts | 29 +++++++++++++++++++++++++---- src/tests.ts | 32 ++++++++++++++++++++++++++------ 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 97ad7bf..a92608b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Property | Description `rubyTestExplorer.debuggerPort` | Define the port to connect the debugger to, for example `1234`. `rubyTestExplorer.debugCommand` | Define how to run rdebug-ide, for example `rdebug-ide` or `bundle exec rdebug-ide`. `rubyTestExplorer.rspecCommand` | Define the command to run RSpec tests with, for example `bundle exec rspec`, `spring rspec`, or `rspec`. -`rubyTestExplorer.rspecDirectory` | Define the relative directory of the specs in a given workspace, for example `./spec/`. +`rubyTestExplorer.rspecDirectory` | Define the relative directory of the specs in a given workspace, for example `./spec/`, or an array of directories like `["./spec/", "./engines/admin/spec/"]`. `rubyTestExplorer.minitestCommand` | Define how to run Minitest with Rake, for example `./bin/rake`, `bundle exec rake` or `rake`. Must be a Rake command. `rubyTestExplorer.minitestDirectory` | Define the relative location of your `test` directory, for example `./test/`. diff --git a/package.json b/package.json index 707db55..133c2e9 100644 --- a/package.json +++ b/package.json @@ -111,9 +111,15 @@ "scope": "resource" }, "rubyTestExplorer.rspecDirectory": { - "markdownDescription": "The location of your RSpec directory relative to the root of the workspace.", + "markdownDescription": "The location of your RSpec directory relative to the root of the workspace. Can be a single path or an array of paths.", "default": "./spec/", - "type": "string", + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, "scope": "resource" }, "rubyTestExplorer.minitestCommand": { diff --git a/src/adapter.ts b/src/adapter.ts index f98c04a..7064988 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -223,24 +223,21 @@ export class RubyAdapter implements TestAdapter { /** * Get the test directory based on the configuration value if there's a configured test framework. */ - private getTestDirectory(): string | undefined { + private getTestDirectories(): string[] { let testFramework = this.getTestFramework(); - let testDirectory = ''; + if (testFramework === 'rspec') { - testDirectory = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) - || path.join('.', 'spec'); + let configured = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string | string[] | undefined; + let directories = Array.isArray(configured) ? configured : [configured || path.join('.', 'spec')]; + return directories.map((directory) => path.join(this.workspace.uri.fsPath, directory)); } else if (testFramework === 'minitest') { - testDirectory = + let testDirectory = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) || path.join('.', 'test'); + return [path.join(this.workspace.uri.fsPath, testDirectory)]; } - if (testDirectory === '') { - return undefined; - } - - return path.join(this.workspace.uri.fsPath, testDirectory); + return []; } /** @@ -256,10 +253,10 @@ export class RubyAdapter implements TestAdapter { const filename = document.uri.fsPath; this.log.info(`${filename} was saved - checking if this effects ${this.workspace.uri.fsPath}`); if (filename.startsWith(this.workspace.uri.fsPath)) { - let testDirectory = this.getTestDirectory(); + let testDirectories = this.getTestDirectories(); // In the case that there's no configured test directory, we shouldn't try to reload the tests. - if (testDirectory !== undefined && filename.startsWith(testDirectory)) { + if (testDirectories.some((testDirectory) => filename.startsWith(testDirectory))) { this.log.info('A test file has been edited, reloading tests.'); this.load(); } diff --git a/src/rspecTests.ts b/src/rspecTests.ts index be92348..4047f6b 100644 --- a/src/rspecTests.ts +++ b/src/rspecTests.ts @@ -104,8 +104,9 @@ export class RspecTests extends Tests { */ protected getTestCommandWithFilePattern(): string { let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); - const dir = this.getTestDirectory(); - let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') + let pattern = this.getTestDirectories() + .flatMap((dir) => this.getFilePattern().map((p) => `${dir}**{,/*/**}/${p}`)) + .join(','); command = command || `bundle exec rspec` return `${command} --pattern '${pattern}'`; } @@ -116,8 +117,28 @@ export class RspecTests extends Tests { * @return The spec directory */ getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); - return directory || './spec/'; + return this.getTestDirectories()[0]; + } + + protected getTestDirectories(): string[] { + let directories = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string | string[] | undefined; + + if (Array.isArray(directories) && directories.length > 0) { + return directories.map((directory) => this.normalizeDirectory(directory)); + } + + return [this.normalizeDirectory((directories as string | undefined) || './spec/')]; + } + + private normalizeDirectory(directory: string): string { + let normalized = directory.replace(/\\/g, '/'); + if (!normalized.startsWith('./')) { + normalized = `./${normalized}`; + } + if (!normalized.endsWith('/')) { + normalized = `${normalized}/`; + } + return normalized; } /** diff --git a/src/tests.ts b/src/tests.ts index e117146..3decb7e 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -108,12 +108,30 @@ export abstract class Tests { } /** - * Get the user-configured test directory, if there is one. + * Get the user-configured primary test directory. * * @return The test directory */ abstract getTestDirectory(): string; + /** + * Get all configured test directories. + * + * Defaults to a single configured directory, but subclasses can override + * this to support multiple roots. + */ + protected getTestDirectories(): string[] { + return [this.getTestDirectory()]; + } + + protected getMatchingTestDirectory(filePath: string): string { + return this.getTestDirectories().find((directory) => filePath.startsWith(directory)) || this.getTestDirectory(); + } + + protected stripTestDirectory(filePath: string): string { + return filePath.replace(this.getMatchingTestDirectory(filePath), ''); + } + /** * Pull JSON out of the test framework output. * @@ -206,10 +224,12 @@ export abstract class Tests { let currentFileLabel = ''; + const matchingDirectory = this.getMatchingTestDirectory(currentFile); + if (directory) { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}${directory}/`, ''); + currentFileLabel = currentFile.replace(`${matchingDirectory}${directory}/`, ''); } else { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}`, ''); + currentFileLabel = currentFile.replace(`${matchingDirectory}`, ''); } let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); @@ -294,7 +314,7 @@ export abstract class Tests { // Remove the spec/ directory from all the file path. uniqueFiles.forEach((file) => { - splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); + splitFilesArray.push(this.stripTestDirectory(file).split('/')); }); // This gets the main types of tests, e.g. features, helpers, models, requests, etc. @@ -312,7 +332,7 @@ export abstract class Tests { let filesInDirectory: Array = []; let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - return file.startsWith(`${this.getTestDirectory()}${directory}/`); + return this.getTestDirectories().some((testDirectory) => file.startsWith(`${testDirectory}${directory}/`)); }); // Get the sets of tests for each file in the current directory. @@ -336,7 +356,7 @@ export abstract class Tests { // Get files that are direct descendants of the spec/ directory. let topDirectoryFiles = uniqueFiles.filter((filePath) => { - return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; + return this.stripTestDirectory(filePath).split('/').length === 1; }); topDirectoryFiles.forEach((currentFile) => {