Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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)
Expand Down Expand Up @@ -57,6 +59,53 @@ export abstract class Config {
return path.resolve(this.workspaceFolder?.uri.fsPath || '.', this.getRelativeTestDirectory())
}

public async findGemfiles(log: IChildLogger): Promise<string[]> {
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
*
Expand Down
10 changes: 5 additions & 5 deletions src/frameworkProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})

Expand Down Expand Up @@ -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)
}
Expand Down
34 changes: 22 additions & 12 deletions src/loaderQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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<vscode.TestItem> {
this.log.debug('enqueing item to resolve: %s', item.id)
public enqueue(item?: vscode.TestItem): Promise<void> {
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<vscode.TestItem>((resolve, reject) => {
if (!item) {
// Load all tests, so clear out anything already in the queue
this.queue.clear()
}
let itemPromise = new Promise<void>((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)
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

Expand Down
23 changes: 21 additions & 2 deletions src/testLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.TestItem> {
return await this.resolveQueue.enqueue(testItem)
public async enqueueItemsForLoading(testItems?: vscode.TestItem | vscode.TestItem[], cancellationToken?: vscode.CancellationToken): Promise<void> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion src/testRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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()
};
Expand Down
1 change: 1 addition & 0 deletions src/testSuiteManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), '')
}
Expand Down
2 changes: 1 addition & 1 deletion test/suite/minitest/minitest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion test/suite/rspec/rspec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down