From 2f47f37a434073c3d7ac10ac2177b7e91d36dbb2 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 1 May 2023 20:55:22 +0100 Subject: [PATCH] Find Gemfile for test if not in project root Also adjusts the cwd for the test process so it can run the tests properly. Still need to pass the relative path to the Gemfile to the process handler so the tests can give the right URIs back --- src/config.ts | 51 +++++++++++++++++++++++++++- src/frameworkProcess.ts | 10 +++--- src/loaderQueue.ts | 34 ++++++++++++------- src/main.ts | 9 ++++- src/testLoader.ts | 23 +++++++++++-- src/testRunner.ts | 2 +- src/testSuiteManager.ts | 1 + test/suite/minitest/minitest.test.ts | 2 +- test/suite/rspec/rspec.test.ts | 2 +- 9 files changed, 110 insertions(+), 24 deletions(-) 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,