diff --git a/src/config.ts b/src/config.ts index 509d775..8ec737a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as childProcess from 'child_process'; -import { IVSCodeExtLogger } from '@vscode-logging/logger'; +import { IChildLogger, IVSCodeExtLogger } from '@vscode-logging/logger'; export abstract class Config { @@ -12,6 +12,8 @@ export abstract class Config { public readonly workspaceFolder?: vscode.WorkspaceFolder; + public gemfilePaths?: string[]; + /** * @param context Either a vscode.ExtensionContext with the extensionUri field set to the location of the extension, * or a string containing the full path to the ruby script dir (the folder containing custom_formatter.rb) @@ -57,6 +59,53 @@ export abstract class Config { return path.resolve(this.workspaceFolder?.uri.fsPath || '.', this.getRelativeTestDirectory()) } + public async findGemfiles(log: IChildLogger): Promise { + if (this.gemfilePaths) return this.gemfilePaths + + let cwd = this.workspaceFolder?.uri.fsPath || path.resolve('.') + let gemfilePatterns = [ + // new vscode.RelativePattern(cwd, path.join('**', 'Gemfile')), + // new vscode.RelativePattern(cwd, path.join('**', 'gems.rb')) + '**/Gemfile', + '**/gems.rb' + ] + let gemfilePaths: string[] = [] + for (const gemfilePattern of gemfilePatterns) { + let uris = await vscode.workspace.findFiles(gemfilePattern, '**/gems/*') + log.debug('Found gemfile uris', uris) + gemfilePaths = gemfilePaths.concat(uris.map(uri => path.relative(cwd, uri.fsPath))) + } + this.gemfilePaths = gemfilePaths + return gemfilePaths + } + + public async findParentGemfileForTests(log: IChildLogger, testPaths: string[]) { + let gemfilePaths = await this.findGemfiles(log) + if (gemfilePaths.length == 1) { + let gemfile = gemfilePaths[0] + log.debug('Only one gemfile found: %s', gemfile) + return path.resolve(path.dirname(gemfile)) + } else if (gemfilePaths.length > 1) { + log.debug('Multiple gemfiles found', gemfilePaths) + for (const gemfile of gemfilePaths) { + let gemfileDir = path.resolve(path.dirname(gemfile)) + let isParent = true + for (const testPath of testPaths) { + const relative = path.relative(gemfileDir, testPath); + if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { + isParent = false + break + } + if (isParent) return gemfileDir + } + } + } else { + log.debug('No gemfiles found') + } + log.warn('No gemfile found that is a parent of tests to be run') + return undefined + } + /** * Gets the arguments to pass to the command from the test items to be run/loaded * diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 6ccb2b3..b7c8814 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -102,12 +102,10 @@ export class FrameworkProcess implements vscode.Disposable { if (data.startsWith('Fast Debugger') && onDebugStarted) { log.info('Notifying debug session that test process is ready to debug'); onDebugStarted() + } else if (this.testRunStarted) { + log.warn('%s', data); } else { - if (this.testRunStarted) { - log.warn('%s', data); - } else { - this.preTestErrorLines.push(data) - } + this.preTestErrorLines.push(data) } }) @@ -148,6 +146,8 @@ export class FrameworkProcess implements vscode.Disposable { let log = this.log.getChildLogger({label: 'onDataReceived'}) let getTest = (testId: string): vscode.TestItem => { + // TODO: Pass dirs between workspace folder and gemfile folder to normaliseTestId + //if (this.spawnArgs.cwd != this. testId = this.testManager.normaliseTestId(testId) return this.testManager.getOrCreateTestItem(testId) } diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts index fbd5690..866ac91 100644 --- a/src/loaderQueue.ts +++ b/src/loaderQueue.ts @@ -8,7 +8,7 @@ import { IChildLogger } from '@vscode-logging/logger'; * reject: Function to call if there is an error loading this test (or the batch it is part of), to reject the associated promise */ export type QueueItem = { - item: vscode.TestItem, + item?: vscode.TestItem, resolve: () => void, reject: (reason?: any) => void } @@ -66,17 +66,21 @@ export class LoaderQueue implements vscode.Disposable { * @returns A promise that is resolved once the test item has been loaded, or which is rejected if there is * an error while loading the item (or the batch containing the item) */ - public enqueue(item: vscode.TestItem): Promise { - this.log.debug('enqueing item to resolve: %s', item.id) + public enqueue(item?: vscode.TestItem): Promise { + this.log.debug('enqueing item to resolve: %s', item?.id || 'all tests') // Create queue item with empty functions let queueItem: QueueItem = { item: item, resolve: () => {}, - reject: () => {}, + reject: () => {} } - let itemPromise = new Promise((resolve, reject) => { + if (!item) { + // Load all tests, so clear out anything already in the queue + this.queue.clear() + } + let itemPromise = new Promise((resolve, reject) => { // Set the resolve & reject functions in the queue item to resolve/reject this promise - queueItem["resolve"] = () => resolve(item) + queueItem["resolve"] = () => resolve() queueItem["reject"] = reject }) this.queue.add(queueItem) @@ -115,13 +119,19 @@ export class LoaderQueue implements vscode.Disposable { let queueItems = Array.from(this.queue) this.queue.clear() - let items = queueItems.map(x => x["item"]) - this.log.debug('worker resolving items', items.map(x => x.id)) try { - // Load tests for items in queue - await this.processItems(items) - // Resolve promises associated with items in queue that have now been loaded - queueItems.map(x => x["resolve"]()) + let allTestsItem = queueItems.filter(x => x["item"] == undefined).at(0) + if (allTestsItem) { + await this.processItems() + allTestsItem.resolve + } else { + let items = queueItems.map(x => x["item"]) + this.log.debug('worker resolving items', items.map(x => x?.id || 'all tests')) + // Load tests for items in queue + await this.processItems(items as vscode.TestItem[]) + // Resolve promises associated with items in queue that have now been loaded + queueItems.map(x => x["resolve"]()) + } } catch (err) { this.log.error("Error resolving tests from queue", err) // Reject promises associated with items in queue that we were trying to load diff --git a/src/main.ts b/src/main.ts index b9c2427..5e87205 100644 --- a/src/main.ts +++ b/src/main.ts @@ -92,13 +92,20 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(profiles.debugProfile); context.subscriptions.push(factory); + controller.refreshHandler = async cancellationToken => { + controller.items.replace([]) + factory.getLoader().dispose() + await factory.getLoader().discoverAllFilesInWorkspace(); + await factory.getLoader().enqueueItemsForLoading(undefined, cancellationToken) + } + controller.resolveHandler = async test => { log.debug('resolveHandler called', test) if (!test) { await factory.getLoader().discoverAllFilesInWorkspace(); } else if (test.canResolveChildren && test.id.endsWith(".rb")) { // Only load files - folders are handled by FileWatchers, and contexts will be loaded when their file is loaded/modified - await factory.getLoader().loadTestItem(test); + await factory.getLoader().enqueueItemsForLoading(test); } }; diff --git a/src/testLoader.ts b/src/testLoader.ts index 3b47382..56d0f11 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -141,8 +141,27 @@ export class TestLoader implements vscode.Disposable { * @param testItem the test item to be loaded * @returns the loaded test item */ - public async loadTestItem(testItem: vscode.TestItem): Promise { - return await this.resolveQueue.enqueue(testItem) + public async enqueueItemsForLoading(testItems?: vscode.TestItem | vscode.TestItem[], cancellationToken?: vscode.CancellationToken): Promise { + let cancellationListener = cancellationToken?.onCancellationRequested((_) => { this.cancellationTokenSource.cancel() }) + if (cancellationListener) { + this.disposables.push(cancellationListener) + } + try { + if (Array.isArray(testItems)) { + let enqueuedItems = [] + for (const item of testItems) { + enqueuedItems.push(this.resolveQueue.enqueue(item)) + } + await Promise.all(enqueuedItems) + } else { + return await this.resolveQueue.enqueue(testItems) + } + } finally { + if (cancellationListener) { + cancellationListener.dispose() + this.disposables.splice(this.disposables.indexOf(cancellationListener), 1) + } + } } private configWatcher(): vscode.Disposable { diff --git a/src/testRunner.ts b/src/testRunner.ts index d713d1e..0bf411e 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -223,7 +223,7 @@ export class TestRunner implements vscode.Disposable { */ private async runTestFramework (testCommand: { command: string, args: string[] }, testRun: vscode.TestRun, profile?: vscode.TestRunProfile): Promise { const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace?.uri.fsPath, + cwd: await this.manager.config.findParentGemfileForTests(this.log, testCommand.args) || this.workspace?.uri.fsPath, shell: true, env: this.manager.config.getProcessEnv() }; diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index 4326e98..4cc8bae 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -72,6 +72,7 @@ export class TestSuiteManager { if (testId.startsWith(`.${path.sep}`)) { testId = testId.substring(2) } + log.debug('Relative', { id: testId, testDir: this.config.getRelativeTestDirectory()}) if (testId.startsWith(this.config.getRelativeTestDirectory())) { testId = testId.replace(this.config.getRelativeTestDirectory(), '') } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 0050c4b..90566da 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -149,7 +149,7 @@ suite('Extension Test for Minitest', function() { ) // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.loadTestItem(absTestItem) + await testLoader.enqueueItemsForLoading(absTestItem) // Tests in that file have now been added to suite testItemCollectionMatches(testController.items, diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 675b72f..cefc863 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -169,7 +169,7 @@ suite('Extension Test for RSpec', function() { ) // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.loadTestItem(absSpecItem) + await testLoader.enqueueItemsForLoading(absSpecItem) // Tests in that file have now been added to suite testItemCollectionMatches(testController.items,