From ad4371eb29c88da8d0634951bf2303dbf99479b2 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 16 Feb 2022 21:57:09 +0000 Subject: [PATCH 001/108] Add new test loader that uses the native Test API --- src/frameworkDetector.ts | 56 ++++++ src/minitest/minitestTestLoader.ts | 14 ++ src/minitest/minitestTests.ts | 256 +++++++++++++++++++++++++ src/rspec/rspecTestLoader.ts | 14 ++ src/rspec/rspecTests.ts | 292 +++++++++++++++++++++++++++++ src/testLoader.ts | 285 ++++++++++++++++++++++++++++ src/testLoaderFactory.ts | 76 ++++++++ 7 files changed, 993 insertions(+) create mode 100644 src/frameworkDetector.ts create mode 100644 src/minitest/minitestTestLoader.ts create mode 100644 src/minitest/minitestTests.ts create mode 100644 src/rspec/rspecTestLoader.ts create mode 100644 src/rspec/rspecTests.ts create mode 100644 src/testLoader.ts create mode 100644 src/testLoaderFactory.ts diff --git a/src/frameworkDetector.ts b/src/frameworkDetector.ts new file mode 100644 index 0000000..666465f --- /dev/null +++ b/src/frameworkDetector.ts @@ -0,0 +1,56 @@ +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; +import { IVSCodeExtLogger } from '@vscode-logging/logger'; + +export function getTestFramework(_log: IVSCodeExtLogger): string { + let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string); + // If the test framework is something other than auto, return the value. + if (['rspec', 'minitest', 'none'].includes(testFramework)) { + return testFramework; + // If the test framework is auto, we need to try to detect the test framework type. + } else { + return detectTestFramework(_log); + } +} + +/** + * Detect the current test framework using 'bundle list'. + */ +function detectTestFramework(_log: IVSCodeExtLogger): string { + _log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); + + const execArgs: childProcess.ExecOptions = { + cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, + maxBuffer: 8192 * 8192 + }; + + try { + // Run 'bundle list' and set the output to bundlerList. + // Execute this syncronously to avoid the test explorer getting stuck loading. + let err, stdout = childProcess.execSync('bundle list', execArgs); + + if (err) { + _log.error(`Error while listing Bundler dependencies: ${err}`); + _log.error(`Output: ${stdout}`); + throw err; + } + + let bundlerList = stdout.toString(); + + // Search for rspec or minitest in the output of 'bundle list'. + // The search function returns the index where the string is found, or -1 otherwise. + if (bundlerList.search('rspec-core') >= 0) { + _log.info(`Detected RSpec test framework.`); + return 'rspec'; + } else if (bundlerList.search('minitest') >= 0) { + _log.info(`Detected Minitest test framework.`); + return 'minitest'; + } else { + _log.info(`Unable to automatically detect a test framework.`); + return 'none'; + } + } catch (error: any) { + _log.error(error); + return 'none'; + } +} diff --git a/src/minitest/minitestTestLoader.ts b/src/minitest/minitestTestLoader.ts new file mode 100644 index 0000000..ae09eb2 --- /dev/null +++ b/src/minitest/minitestTestLoader.ts @@ -0,0 +1,14 @@ +import { TestLoader } from '../testLoader'; +import * as vscode from 'vscode'; +import * as path from 'path'; + +export class MinitestTestLoader extends TestLoader { + protected frameworkName(): string { + return "Minitest" + } + + protected getFrameworkTestDirectory(): string { + return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) + || path.join('.', 'test'); + } +} \ No newline at end of file diff --git a/src/minitest/minitestTests.ts b/src/minitest/minitestTests.ts new file mode 100644 index 0000000..0a71563 --- /dev/null +++ b/src/minitest/minitestTests.ts @@ -0,0 +1,256 @@ +import * as vscode from 'vscode'; +import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; +import * as childProcess from 'child_process'; +import { Tests } from '../tests'; + +export class MinitestTests extends Tests { + testFrameworkName = 'Minitest'; + + /** + * Representation of the Minitest test suite as a TestSuiteInfo object. + * + * @return The Minitest test suite as a TestSuiteInfo object. + */ + tests = async () => new Promise((resolve, reject) => { + try { + // If test suite already exists, use testSuite. Otherwise, load them. + let minitestTests = this.testSuite ? this.testSuite : this.loadTests(); + return resolve(minitestTests); + } catch (err) { + if (err instanceof Error) { + this.log.error(`Error while attempting to load Minitest tests: ${err.message}`); + return reject(err); + } + } + }); + + /** + * Perform a dry-run of the test suite to get information about every test. + * + * @return The raw output from the Minitest JSON formatter. + */ + initTests = async () => new Promise((resolve, reject) => { + let cmd = `${this.getTestCommand()} vscode:minitest:list`; + + // Allow a buffer of 64MB. + const execArgs: childProcess.ExecOptions = { + cwd: this.workspace.uri.fsPath, + maxBuffer: 8192 * 8192, + env: this.getProcessEnv() + }; + + this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); + + childProcess.exec(cmd, execArgs, (err, stdout) => { + if (err) { + this.log.error(`Error while finding Minitest test suite: ${err.message}`); + this.log.error(`Output: ${stdout}`); + // Show an error message. + vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); + vscode.window.showErrorMessage(err.message); + throw err; + } + resolve(stdout); + }); + }); + + /** + * Get the user-configured Minitest command, if there is one. + * + * @return The Minitest command + */ + protected getTestCommand(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; + return `${command} -R ${(process.platform == 'win32') ? '%EXT_DIR%' : '$EXT_DIR'}`; + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_minitest.rb` + ); + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The test directory + */ + getTestDirectory(): string { + let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); + return directory || './test/'; + } + + /** + * Get the absolute path of the custom_formatter.rb file. + * + * @return The spec directory + */ + protected getRubyScriptsLocation(): string { + return this.context.asAbsolutePath('./ruby'); + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + protected getProcessEnv(): any { + return Object.assign({}, process.env, { + "RAILS_ENV": "test", + "EXT_DIR": this.getRubyScriptsLocation(), + "TESTS_DIR": this.getTestDirectory(), + "TESTS_PATTERN": this.getFilePattern().join(',') + }); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The test command + */ + protected testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + let cmd = `${this.getTestCommand()} vscode:minitest:run` + if (debuggerConfig) { + cmd = this.getDebugCommand(debuggerConfig); + } + return cmd; + } + + /** + * Runs a single test. + * + * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test. + */ + runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running single test: ${testLocation}`); + let line = testLocation.split(':').pop(); + let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace.uri.fsPath}/`, "") + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true, + env: this.getProcessEnv() + }; + + let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeLocation}:${line}'`; + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Runs tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the tests. + */ + runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running test file: ${testFile}`); + let relativeFile = testFile.replace(`${this.workspace.uri.fsPath}/`, "").replace(`./`, "") + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true, + env: this.getProcessEnv() + }; + + // Run tests for a given file at once with a single command. + let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeFile}'`; + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Runs the full test suite for the current workspace. + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test suite. + */ + runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running full test suite.`); + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true, + env: this.getProcessEnv() + }; + + let testCommand = this.testCommandWithDebugger(debuggerConfig); + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Handles test state based on the output returned by the Minitest Rake task. + * + * @param test The test that we want to handle. + */ + handleStatus(test: any): void { + this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + if (test.status === "passed") { + this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); + } else if (test.status === "failed" && test.pending_message === null) { + let errorMessageShort: string = test.exception.message; + let errorMessageLine: number = test.line_number; + let errorMessage: string = test.exception.message; + + if (test.exception.position) { + errorMessageLine = test.exception.position; + } + + // Add backtrace to errorMessage if it exists. + if (test.exception.backtrace) { + errorMessage += `\n\nBacktrace:\n`; + test.exception.backtrace.forEach((line: string) => { + errorMessage += `${line}\n`; + }); + errorMessage += `\n\nFull Backtrace:\n`; + test.exception.full_backtrace.forEach((line: string) => { + errorMessage += `${line}\n`; + }); + } + + this.testStatesEmitter.fire({ + type: 'test', + test: test.id, + state: 'failed', + message: errorMessage, + decorations: [{ + message: errorMessageShort, + line: errorMessageLine - 1 + }] + }); + } else if (test.status === "failed" && test.pending_message !== null) { + // Handle pending test cases. + this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); + } + }; +} diff --git a/src/rspec/rspecTestLoader.ts b/src/rspec/rspecTestLoader.ts new file mode 100644 index 0000000..bcacf9a --- /dev/null +++ b/src/rspec/rspecTestLoader.ts @@ -0,0 +1,14 @@ +import { TestLoader } from '../testLoader'; +import * as vscode from 'vscode'; +import * as path from 'path'; + +export class RspecTestLoader extends TestLoader { + protected frameworkName(): string { + return "RSpec" + } + + protected getFrameworkTestDirectory(): string { + return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) + || path.join('.', 'spec'); + } +} \ No newline at end of file diff --git a/src/rspec/rspecTests.ts b/src/rspec/rspecTests.ts new file mode 100644 index 0000000..74f4a42 --- /dev/null +++ b/src/rspec/rspecTests.ts @@ -0,0 +1,292 @@ +import * as vscode from 'vscode'; +import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; +import * as childProcess from 'child_process'; +import { Tests } from '../tests'; + +export class RspecTests extends Tests { + /** + * Representation of the RSpec test suite as a TestSuiteInfo object. + * + * @return The RSpec test suite as a TestSuiteInfo object. + */ + tests = async () => new Promise((resolve, reject) => { + try { + // If test suite already exists, use testSuite. Otherwise, load them. + let rspecTests = this.testSuite ? this.testSuite : this.loadTests(); + return resolve(rspecTests); + } catch (err) { + if (err instanceof Error) { + this.log.error(`Error while attempting to load RSpec tests: ${err.message}`); + return reject(err); + } + } + }); + + /** + * Perform a dry-run of the test suite to get information about every test. + * + * @return The raw output from the RSpec JSON formatter. + */ + initTests = async (/*testFilePath: string | null*/) => new Promise((resolve, reject) => { + let cmd = `${this.getTestCommandWithFilePattern()} --require ${this.getCustomFormatterLocation()}` + + ` --format CustomFormatter --order defined --dry-run`; + + // TODO: Only reload single file on file changed + // if (testFilePath) { + // cmd = cmd + ` ${testFilePath}` + // } + + this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); + + // Allow a buffer of 64MB. + const execArgs: childProcess.ExecOptions = { + cwd: this.workspace.uri.fsPath, + maxBuffer: 8192 * 8192, + }; + + childProcess.exec(cmd, execArgs, (err, stdout) => { + if (err) { + this.log.error(`Error while finding RSpec test suite: ${err.message}`); + // Show an error message. + vscode.window.showWarningMessage( + "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", + "View error message" + ).then(selection => { + if (selection === "View error message") { + let outputJson = JSON.parse(Tests.getJsonFromOutput(stdout)); + let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); + + if (outputJson.messages.length > 0) { + let outputJsonString = outputJson.messages.join("\n\n"); + let outputJsonArray = outputJsonString.split("\n"); + outputJsonArray.forEach((line: string) => { + outputChannel.appendLine(line); + }) + } else { + outputChannel.append(err.message); + } + outputChannel.show(false); + } + }); + + throw err; + } + resolve(stdout); + }); + }); + + /** + * Get the user-configured RSpec command, if there is one. + * + * @return The RSpec command + */ + protected getTestCommand(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); + return command || `bundle exec rspec` + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` + ); + } + /** + * Get the user-configured RSpec command and add file pattern detection. + * + * @return The RSpec command + */ + 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(',') + command = command || `bundle exec rspec` + return `${command} --pattern '${pattern}'`; + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The spec directory + */ + getTestDirectory(): string { + let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); + return directory || './spec/'; + } + + /** + * Get the absolute path of the custom_formatter.rb file. + * + * @return The spec directory + */ + protected getCustomFormatterLocation(): string { + return this.context.asAbsolutePath('./custom_formatter.rb'); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The test command + */ + protected testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` + let cmd = `${this.getTestCommand()} ${args}` + if (debuggerConfig) { + cmd = this.getDebugCommand(debuggerConfig, args); + } + return cmd + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + protected getProcessEnv(): any { + return Object.assign({}, process.env, { + "EXT_DIR": this.getRubyScriptsLocation(), + }); + } + + /** + * Runs a single test. + * + * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test. + */ + runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running single test: ${testLocation}`); + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true, + env: this.getProcessEnv() + }; + + let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testLocation}'`; + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Runs tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/spec.rb`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the tests. + */ + runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running test file: ${testFile}`); + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true + }; + + // Run tests for a given file at once with a single command. + let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testFile}'`; + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Runs the full test suite for the current workspace. + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test suite. + */ + runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { + this.log.info(`Running full test suite.`); + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace.uri.fsPath, + shell: true + }; + + let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + resolve(await this.handleChildProcess(testProcess)); + }); + + /** + * Handles test state based on the output returned by the custom RSpec formatter. + * + * @param test The test that we want to handle. + */ + handleStatus(test: any): void { + this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + if (test.status === "passed") { + this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); + } else if (test.status === "failed" && test.pending_message === null) { + // Remove linebreaks from error message. + let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); + // Prepend the class name to the error message string. + let errorMessage: string = `${test.exception.class}:\n${errorMessageNoLinebreaks}`; + + let fileBacktraceLineNumber: number | undefined; + + let filePath = test.file_path.replace('./', ''); + + // Add backtrace to errorMessage if it exists. + if (test.exception.backtrace) { + errorMessage += `\n\nBacktrace:\n`; + test.exception.backtrace.forEach((line: string) => { + errorMessage += `${line}\n`; + // If the backtrace line includes the current file path, try to get the line number from it. + if (line.includes(filePath)) { + let filePathArray = filePath.split('/'); + let fileName = filePathArray[filePathArray.length - 1]; + // Input: spec/models/game_spec.rb:75:in `block (3 levels) in + // Output: 75 + let regex = new RegExp(`${fileName}\:(\\d+)`); + let match = line.match(regex); + if (match && match[1]) { + fileBacktraceLineNumber = parseInt(match[1]); + } + } + }); + } + + this.testStatesEmitter.fire({ + type: 'test', + test: test.id, + state: 'failed', + message: errorMessage, + decorations: [{ + // Strip line breaks from the message. + message: errorMessageNoLinebreaks, + line: (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1 + }] + }); + } else if (test.status === "failed" && test.pending_message !== null) { + // Handle pending test cases. + this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); + } + }; +} diff --git a/src/testLoader.ts b/src/testLoader.ts new file mode 100644 index 0000000..8888078 --- /dev/null +++ b/src/testLoader.ts @@ -0,0 +1,285 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { IVSCodeExtLogger } from '@vscode-logging/logger'; +import { Tests } from './tests'; +import { RspecTests } from './rspec/rspecTests'; +import { MinitestTests } from './minitest/minitestTests'; + +export abstract class TestLoader implements vscode.Disposable { + protected _disposables: { dispose(): void }[] = []; + + constructor( + protected readonly log: IVSCodeExtLogger, + protected readonly context: vscode.ExtensionContext, + protected readonly workspace: vscode.WorkspaceFolder | null, + protected readonly controller: vscode.TestController, + protected readonly testRunner: RspecTests | MinitestTests + ) { + this._disposables.push(this.createWatcher()); + this._disposables.push(this.configWatcher()); + } + + dispose(): void { + for (const disposable of this._disposables) { + disposable.dispose(); + } + this._disposables = []; + } + + /** + * Printable name of the test framework + */ + protected abstract frameworkName(): string + + /** + * Path in which to look for test files for the test framework in use + */ + protected abstract getFrameworkTestDirectory(): string + + /** + * Takes the output from initTests() and parses the resulting + * JSON into a TestSuiteInfo object. + * + * @return The full test suite. + */ + public async loadAllTests(): Promise { + this.log.info(`Loading Ruby tests (${this.frameworkName()})...`); + let output = await this.testRunner.initTests(); + this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); + this.log.debug(`${output}`); + output = Tests.getJsonFromOutput(output); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${output}`); + let testMetadata; + try { + testMetadata = JSON.parse(output); + } catch (error) { + this.log.error(`JSON parsing failed: ${error}`); + } + + let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; + + testMetadata.examples.forEach( + (test: { id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }) => { + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let test_location_string: string = test_location_array.join(''); + test.location = parseInt(test_location_string); + tests.push(test); + } + ); + + let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); + + // // Sort the children of each test suite based on their location in the test tree. + // testSuite.forEach((suite: vscode.TestItem) => { + // // NOTE: This will only sort correctly if everything is nested at the same + // // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is + // // introduced, the location is generated as e.g. 1231, which won't + // // sort properly relative to everything else. + // (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { + // if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { + // let aLocation: number = this.getTestLocation(a as TestInfo); + // let bLocation: number = this.getTestLocation(b as TestInfo); + // return aLocation - bLocation; + // } else { + // return 0; + // } + // }) + // }); + + this.controller.items.replace(testSuite); + } + + /** + * Get the test directory based on the configuration value if there's a configured test framework. + */ + private getTestDirectory(): string | undefined { + let testDirectory = this.getFrameworkTestDirectory(); + + if (testDirectory === '' || !this.workspace) { + return undefined; + } + + return path.join(this.workspace.uri.fsPath, testDirectory); + } + + /** + * Create the base test suite with a root node and one layer of child nodes + * representing the subdirectories of spec/, and then any files under the + * given subdirectory. + * + * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. + * @return The test suite root with its children. + */ + private async getBaseTestSuite(tests: any[]): Promise { + let testSuite: vscode.TestItem[] = [] + + // Create an array of all test files and then abuse Sets to make it unique. + let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; + + let splitFilesArray: Array = []; + + // Remove the spec/ directory from all the file path. + uniqueFiles.forEach((file) => { + splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); + }); + + // This gets the main types of tests, e.g. features, helpers, models, requests, etc. + let subdirectories: Array = []; + splitFilesArray.forEach((splitFile) => { + if (splitFile.length > 1) { + subdirectories.push(splitFile[0]); + } + }); + subdirectories = [...new Set(subdirectories)]; + + // A nested loop to iterate through the direct subdirectories of spec/ and then + // organize the files under those subdirectories. + subdirectories.forEach((directory) => { + let dirPath = `${this.getTestDirectory()}${directory}/` + let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { + return file.startsWith(dirPath); + }); + + let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(dirPath)); + + // Get the sets of tests for each file in the current directory. + uniqueFilesInDirectory.forEach((currentFile: string) => { + let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile, directory }); + directoryTestSuite.children.add(currentFileTestSuite); + }); + + testSuite.push(directoryTestSuite); + }); + + // Sort test suite types alphabetically. + //testSuite = this.sortTestSuiteChildren(testSuite); + + // Get files that are direct descendants of the spec/ directory. + let topDirectoryFiles = uniqueFiles.filter((filePath) => { + return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; + }); + + topDirectoryFiles.forEach((currentFile) => { + let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); + testSuite.push(currentFileTestSuite); + }); + + return testSuite; + } + + /** + * Get the tests in a given file. + */ + public getTestSuiteForFile( + { tests, currentFile, directory }: { + tests: Array<{ + id: string; + full_description: string; + description: string; + file_path: string; + line_number: number; + location: number; + }>; currentFile: string; directory?: string; + }): vscode.TestItem { + let currentFileTests = tests.filter(test => { + return test.file_path === currentFile + }); + + let currentFileLabel = directory + ? currentFile.replace(`${this.getTestDirectory()}${directory}/`, '') + : currentFile.replace(`${this.getTestDirectory()}`, ''); + + let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); + + // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", + // but with the latter's first character stripped. + let currentFileAsAbsolutePath = `${this.workspace?.uri.fsPath}${currentFile.substring(1)}`; + + let currentFileTestSuite: vscode.TestItem = this.controller.createTestItem( + currentFile, + currentFileLabel, + vscode.Uri.file(currentFileAsAbsolutePath) + ); + + currentFileTests.forEach((test) => { + // RSpec provides test ids like "file_name.rb[1:2:3]". + // This uses the digits at the end of the id to create + // an array of numbers representing the location of the + // test in the file. + let testLocationArray: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').map((x) => { + return parseInt(x); + }); + + // Get the last element in the location array. + let testNumber: number = testLocationArray[testLocationArray.length - 1]; + // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" + // is appended to the test description to distinguish between separate tests. + let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; + + // If the current file label doesn't have a slash in it and it starts with the PascalCase'd + // file name, remove the from the start of the description. This turns, e.g. + // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. + if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { + // Optional check for a space following the PascalCase file name. In some + // cases, e.g. 'FileName#method_name` there's no space after the file name. + let regexString = `${pascalCurrentFileLabel}[ ]?`; + let regex = new RegExp(regexString, "g"); + description = description.replace(regex, ''); + } + + let testItem = this.controller.createTestItem(test.id, description, vscode.Uri.file(currentFileAsAbsolutePath)); + testItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); + + currentFileTestSuite.children.add(testItem); + }); + + return currentFileTestSuite; + } + + /** + * Convert a string from snake_case to PascalCase. + * Note that the function will return the input string unchanged if it + * includes a '/'. + * + * @param string The string to convert to PascalCase. + * @return The converted string. + */ + private snakeToPascalCase(string: string): string { + if (string.includes('/')) { return string } + return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); + } + + /** + * Create a file watcher that will reload the test tree when a relevant file is changed. + */ + private createWatcher(): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(document => { + if (!this.workspace) + return + + 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(); + + // In the case that there's no configured test directory, we shouldn't try to reload the tests. + if (testDirectory !== undefined && filename.startsWith(testDirectory)) { + this.log.info('A test file has been edited, reloading tests.'); + + // TODO: Reload only single file + this.loadAllTests(); + } + } + }) + } + + private configWatcher(): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(configChange => { + this.log.info('Configuration changed'); + if (configChange.affectsConfiguration("rubyTestExplorer")) { + this.loadAllTests(); + } + }) + } +} \ No newline at end of file diff --git a/src/testLoaderFactory.ts b/src/testLoaderFactory.ts new file mode 100644 index 0000000..83ad1f5 --- /dev/null +++ b/src/testLoaderFactory.ts @@ -0,0 +1,76 @@ +import * as vscode from 'vscode'; +import { TestLoader } from "./testLoader"; +import { getTestFramework } from './frameworkDetector'; +import { IVSCodeExtLogger } from "@vscode-logging/logger"; +import { RspecTestLoader } from './rspec/rspecTestLoader'; +import { MinitestTestLoader } from './minitest/minitestTestLoader'; +import { RspecTests } from './rspec/rspecTests'; +import { MinitestTests } from './minitest/minitestTests'; + +export class TestLoaderFactory implements vscode.Disposable { + private _instance: TestLoader | null = null; + protected disposables: { dispose(): void }[] = []; + + constructor( + protected readonly _log: IVSCodeExtLogger, + protected readonly _context: vscode.ExtensionContext, + protected readonly _workspace: vscode.WorkspaceFolder, + protected readonly _controller: vscode.TestController + ) { + this.disposables.push(this.configWatcher()); + } + + dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); + } + this.disposables = []; + } + + public getLoader(): TestLoader { + if (this._instance) { + return this._instance; + } + + let framework = getTestFramework(this._log); + switch(framework) { + case "rspec": + return new RspecTestLoader( + this._log, + this._context, + this._workspace, + this._controller, + new RspecTests( + this._context, + this._log, + this._workspace, + this._controller + ) + ); + case "minitest": + return new MinitestTestLoader( + this._log, + this._context, + this._workspace, + this._controller, + new MinitestTests( + this._context, + this._log, + this._workspace, + this._controller + )); + default: + throw `Unknown framework ${framework}` + } + } + + private configWatcher(): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(configChange => { + this._log.info('Configuration changed'); + if (configChange.affectsConfiguration("rubyTestExplorer")) { + this._instance?.dispose() + this._instance = null + } + }) + } +} \ No newline at end of file From 966a878d6da95dad6b0e701e9aa0521f10a6497b Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 16 Feb 2022 22:42:54 +0000 Subject: [PATCH 002/108] Change dependencies for new API --- package-lock.json | 836 ++++++++++++++++++++++++++++++++++++++++------ package.json | 5 +- 2 files changed, 735 insertions(+), 106 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4148a84..31a0ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,14 +5,12 @@ "requires": true, "packages": { "": { - "name": "vscode-ruby-test-adapter", "version": "0.9.0", "license": "MIT", "dependencies": { + "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.2.0", - "vscode-test-adapter-api": "^1.9.0", - "vscode-test-adapter-util": "^0.7.1" + "tslib": "^2.2.0" }, "devDependencies": { "@types/glob": "^7.1.3", @@ -30,6 +28,24 @@ "vscode": "^1.54.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -88,6 +104,27 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "node_modules/@vscode-logging/logger": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vscode-logging/logger/-/logger-1.2.3.tgz", + "integrity": "sha512-kQgRjbZmBBUktklaEat8YtTBuddCNRJezjJTk0Y+4VIs49KWjYkfF02GoyiCYASvCSMSqw9bBTQN3ijCO2aq+A==", + "dependencies": { + "@vscode-logging/types": "^0.1.4", + "fast-safe-stringify": "2.0.7", + "fs-extra": "9.1.0", + "lodash": "^4.17.21", + "stacktrace-js": "2.0.2", + "streamroller": "2.2.3", + "triple-beam": "1.3.0", + "winston": "3.3.3", + "winston-transport": "4.3.0" + } + }, + "node_modules/@vscode-logging/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", + "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -152,6 +189,19 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/azure-devops-node-api": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", @@ -376,6 +426,7 @@ "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", + "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -400,6 +451,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -415,8 +475,38 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "node_modules/commander": { "version": "6.2.1", @@ -436,8 +526,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/css-select": { "version": "4.1.3", @@ -467,11 +556,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -487,8 +583,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decamelize": { "version": "4.0.0", @@ -587,6 +682,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -596,6 +696,14 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-stack-parser": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", + "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", + "dependencies": { + "stackframe": "^1.1.1" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -617,6 +725,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -626,6 +739,11 @@ "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", + "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -663,6 +781,25 @@ "flat": "cli.js" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -774,8 +911,7 @@ "node_modules/graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/growl": { "version": "1.10.5", @@ -899,8 +1035,12 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -962,6 +1102,17 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -977,8 +1128,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "node_modules/isexe": { "version": "2.0.0", @@ -998,6 +1148,23 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -1040,8 +1207,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/log-symbols": { "version": "4.1.0", @@ -1059,6 +1225,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logform": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz", + "integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==", + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1216,8 +1394,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -1276,6 +1453,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -1397,8 +1582,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/qs": { "version": "6.10.1", @@ -1440,7 +1624,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1454,8 +1637,7 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/readdirp": { "version": "3.6.0", @@ -1513,6 +1695,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "engines": { + "node": ">=10" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -1557,6 +1747,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -1571,11 +1777,95 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "node_modules/stack-generator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", + "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", + "dependencies": { + "stackframe": "^1.1.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "engines": { + "node": "*" + } + }, + "node_modules/stackframe": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", + "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" + }, + "node_modules/stacktrace-gps": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz", + "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.1.1" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/streamroller": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", + "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "dependencies": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dependencies": { + "graceful-fs": "^4.1.6" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -1583,8 +1873,7 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/string-width": { "version": "4.2.3", @@ -1639,6 +1928,11 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -1672,6 +1966,11 @@ "node": "*" } }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -1722,6 +2021,14 @@ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unzipper": { "version": "0.10.11", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", @@ -1749,8 +2056,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/vsce": { "version": "1.100.2", @@ -1874,31 +2180,6 @@ "node": ">=8.9.3" } }, - "node_modules/vscode-test-adapter-api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz", - "integrity": "sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ==", - "engines": { - "vscode": "^1.23.0" - } - }, - "node_modules/vscode-test-adapter-util": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz", - "integrity": "sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==", - "dependencies": { - "tslib": "^1.11.1", - "vscode-test-adapter-api": "^1.8.0" - }, - "engines": { - "vscode": "^1.24.0" - } - }, - "node_modules/vscode-test-adapter-util/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1914,6 +2195,63 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "dependencies": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "dependencies": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, "node_modules/workerpool": { "version": "6.1.5", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", @@ -2055,6 +2393,21 @@ } }, "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2110,6 +2463,27 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "@vscode-logging/logger": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@vscode-logging/logger/-/logger-1.2.3.tgz", + "integrity": "sha512-kQgRjbZmBBUktklaEat8YtTBuddCNRJezjJTk0Y+4VIs49KWjYkfF02GoyiCYASvCSMSqw9bBTQN3ijCO2aq+A==", + "requires": { + "@vscode-logging/types": "^0.1.4", + "fast-safe-stringify": "2.0.7", + "fs-extra": "9.1.0", + "lodash": "^4.17.21", + "stacktrace-js": "2.0.2", + "streamroller": "2.2.3", + "triple-beam": "1.3.0", + "winston": "3.3.3", + "winston-transport": "4.3.0" + } + }, + "@vscode-logging/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", + "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2156,6 +2530,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, "azure-devops-node-api": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", @@ -2350,6 +2734,30 @@ "wrap-ansi": "^7.0.0" } }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2362,8 +2770,25 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "commander": { "version": "6.2.1", @@ -2380,8 +2805,7 @@ "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "css-select": { "version": "4.1.3", @@ -2402,11 +2826,15 @@ "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", "dev": true }, + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" + }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, "requires": { "ms": "2.1.2" }, @@ -2414,8 +2842,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -2489,12 +2916,25 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true }, + "error-stack-parser": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", + "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", + "requires": { + "stackframe": "^1.1.1" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2507,6 +2947,11 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -2516,6 +2961,11 @@ "pend": "~1.2.0" } }, + "fecha": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", + "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2541,6 +2991,22 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2626,8 +3092,7 @@ "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "growl": { "version": "1.10.5", @@ -2717,8 +3182,12 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-binary-path": { "version": "2.1.0", @@ -2762,6 +3231,11 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -2771,8 +3245,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2789,6 +3262,20 @@ "argparse": "^2.0.1" } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -2822,8 +3309,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "log-symbols": { "version": "4.1.0", @@ -2835,6 +3321,18 @@ "is-unicode-supported": "^0.1.0" } }, + "logform": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz", + "integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2961,8 +3459,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mute-stream": { "version": "0.0.8", @@ -3006,6 +3503,14 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -3097,8 +3602,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "qs": { "version": "6.10.1", @@ -3131,7 +3635,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3145,8 +3648,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -3180,6 +3682,11 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -3218,6 +3725,19 @@ "object-inspect": "^1.9.0" } }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, "split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -3229,11 +3749,82 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "stack-generator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", + "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", + "requires": { + "stackframe": "^1.1.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stackframe": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", + "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" + }, + "stacktrace-gps": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz", + "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.1.1" + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "streamroller": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", + "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" }, @@ -3241,8 +3832,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -3281,6 +3871,11 @@ "has-flag": "^4.0.0" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -3305,6 +3900,11 @@ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", "dev": true }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -3345,6 +3945,11 @@ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, "unzipper": { "version": "0.10.11", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", @@ -3372,8 +3977,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "vsce": { "version": "1.100.2", @@ -3475,34 +4079,60 @@ "unzipper": "^0.10.11" } }, - "vscode-test-adapter-api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz", - "integrity": "sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ==" + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } }, - "vscode-test-adapter-util": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz", - "integrity": "sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==", + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", "requires": { - "tslib": "^1.11.1", - "vscode-test-adapter-api": "^1.8.0" + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" }, "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } } } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", "requires": { - "isexe": "^2.0.0" + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" } }, "workerpool": { diff --git a/package.json b/package.json index 1d345b3..06cd546 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,9 @@ "test:rspec": "npm run build && node ./out/test/runRspecTests.js" }, "dependencies": { + "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.2.0", - "vscode-test-adapter-api": "^1.9.0", - "vscode-test-adapter-util": "^0.7.1" + "tslib": "^2.2.0" }, "devDependencies": { "@types/glob": "^7.1.3", From 235ca4b0ed2ec429aad79ebeddc43573a1cbad32 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 17 Feb 2022 02:22:30 +0000 Subject: [PATCH 003/108] Convert base test runner class to new API --- src/main.ts | 77 ++- .../minitestTestRunner.ts} | 5 +- src/minitest/minitestTests.ts | 256 -------- .../{rspecTests.ts => rspecTestRunner.ts} | 7 +- src/rspecTests.ts | 289 --------- src/testFactory.ts | 103 ++++ src/testLoader.ts | 10 +- src/testLoaderFactory.ts | 76 --- src/testRunner.ts | 323 ++++++++++ src/tests.ts | 554 ------------------ 10 files changed, 491 insertions(+), 1209 deletions(-) rename src/{minitestTests.ts => minitest/minitestTestRunner.ts} (98%) delete mode 100644 src/minitest/minitestTests.ts rename src/rspec/{rspecTests.ts => rspecTestRunner.ts} (97%) delete mode 100644 src/rspecTests.ts create mode 100644 src/testFactory.ts delete mode 100644 src/testLoaderFactory.ts create mode 100644 src/testRunner.ts delete mode 100644 src/tests.ts diff --git a/src/main.ts b/src/main.ts index 6be7a9d..1223c19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,32 +1,65 @@ import * as vscode from 'vscode'; -import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api'; -import { Log, TestAdapterRegistrar } from 'vscode-test-adapter-util'; -import { RubyAdapter } from './adapter'; +import { getExtensionLogger } from "@vscode-logging/logger"; +import { getTestFramework } from './frameworkDetector'; +import { TestFactory } from './testFactory'; export async function activate(context: vscode.ExtensionContext) { - // Determine whether to send the logger a workspace. - let logWorkspaceFolder = (vscode.workspace.workspaceFolders || [])[0]; - // create a simple logger that can be configured with the configuration variables - // `rubyTestExplorer.logpanel` and `rubyTestExplorer.logfile` - let log = new Log('rubyTestExplorer', logWorkspaceFolder, 'Ruby Test Explorer Log'); - context.subscriptions.push(log); + let config = vscode.workspace.getConfiguration('rubyTestExplorer', null) + + const log = getExtensionLogger({ + extName: "RubyTestExplorer", + level: "info", // See LogLevel type in @vscode-logging/types for possible logLevels + logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` + logOutputChannel: vscode.window.createOutputChannel("Ruby Test Explorer log"), // OutputChannel for the logger + sourceLocationTracking: false, + logConsole: (config.get('logPanel') as boolean) // define if messages should be logged to the consol + }); + if (vscode.workspace.workspaceFolders == undefined) { + log.error("No workspace opened") + } + + const workspace: vscode.WorkspaceFolder | null = vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0] + : null; + let testFramework: string = getTestFramework(log); - // get the Test Explorer extension - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId); - if (log.enabled) { - log.info(`Test Explorer ${testExplorerExtension ? '' : 'not '}found`); + const debuggerConfig: vscode.DebugConfiguration = { + name: "Debug Ruby Tests", + type: "Ruby", + request: "attach", + remoteHost: config.get('debuggerHost') || "127.0.0.1", + remotePort: config.get('debuggerPort') || "1234", + remoteWorkspaceRoot: "${workspaceRoot}" } - let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string) || 'none'; + if (testFramework !== "none") { + const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); + const testLoaderFactory = new TestFactory(log, context, workspace, controller); + context.subscriptions.push(controller); - if (testExplorerExtension && testFramework !== "none") { - const testHub = testExplorerExtension.exports; + testLoaderFactory.getLoader().loadAllTests(); - // this will register a RubyTestAdapter for each WorkspaceFolder - context.subscriptions.push(new TestAdapterRegistrar( - testHub, - workspaceFolder => new RubyAdapter(workspaceFolder, log, context), - log - )); + // Custom handler for loading tests. The "test" argument here is undefined, + // but if we supported lazy-loading child test then this could be called with + // the test whose children VS Code wanted to load. + controller.resolveHandler = test => { + controller.items.replace([]); // TODO: Load tests + }; + + // TODO: (?) Add a "Profile" profile for profiling tests + controller.createRunProfile( + 'Run', + vscode.TestRunProfileKind.Run, + (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + true // Default run profile + ); + controller.createRunProfile( + 'Debug', + vscode.TestRunProfileKind.Debug, + (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig) + ); + } + else { + log.fatal('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); } } diff --git a/src/minitestTests.ts b/src/minitest/minitestTestRunner.ts similarity index 98% rename from src/minitestTests.ts rename to src/minitest/minitestTestRunner.ts index da92249..89f2c83 100644 --- a/src/minitestTests.ts +++ b/src/minitest/minitestTestRunner.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; import * as childProcess from 'child_process'; -import { Tests } from './tests'; +import { TestRunner } from '../testRunner'; -export class MinitestTests extends Tests { +export class MinitestTestRunner extends TestRunner { testFrameworkName = 'Minitest'; /** diff --git a/src/minitest/minitestTests.ts b/src/minitest/minitestTests.ts deleted file mode 100644 index 0a71563..0000000 --- a/src/minitest/minitestTests.ts +++ /dev/null @@ -1,256 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import { Tests } from '../tests'; - -export class MinitestTests extends Tests { - testFrameworkName = 'Minitest'; - - /** - * Representation of the Minitest test suite as a TestSuiteInfo object. - * - * @return The Minitest test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let minitestTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(minitestTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load Minitest tests: ${err.message}`); - return reject(err); - } - } - }); - - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the Minitest JSON formatter. - */ - initTests = async () => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommand()} vscode:minitest:list`; - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192, - env: this.getProcessEnv() - }; - - this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); - - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding Minitest test suite: ${err.message}`); - this.log.error(`Output: ${stdout}`); - // Show an error message. - vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); - vscode.window.showErrorMessage(err.message); - throw err; - } - resolve(stdout); - }); - }); - - /** - * Get the user-configured Minitest command, if there is one. - * - * @return The Minitest command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; - return `${command} -R ${(process.platform == 'win32') ? '%EXT_DIR%' : '$EXT_DIR'}`; - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_minitest.rb` - ); - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); - return directory || './test/'; - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return this.context.asAbsolutePath('./ruby'); - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "RAILS_ENV": "test", - "EXT_DIR": this.getRubyScriptsLocation(), - "TESTS_DIR": this.getTestDirectory(), - "TESTS_PATTERN": this.getFilePattern().join(',') - }); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let cmd = `${this.getTestCommand()} vscode:minitest:run` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig); - } - return cmd; - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); - let line = testLocation.split(':').pop(); - let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace.uri.fsPath}/`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeLocation}:${line}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - let relativeFile = testFile.replace(`${this.workspace.uri.fsPath}/`, "").replace(`./`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = this.testCommandWithDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Handles test state based on the output returned by the Minitest Rake task. - * - * @param test The test that we want to handle. - */ - handleStatus(test: any): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); - if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); - } else if (test.status === "failed" && test.pending_message === null) { - let errorMessageShort: string = test.exception.message; - let errorMessageLine: number = test.line_number; - let errorMessage: string = test.exception.message; - - if (test.exception.position) { - errorMessageLine = test.exception.position; - } - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - errorMessage += `\n\nFull Backtrace:\n`; - test.exception.full_backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - } - - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - message: errorMessageShort, - line: errorMessageLine - 1 - }] - }); - } else if (test.status === "failed" && test.pending_message !== null) { - // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); - } - }; -} diff --git a/src/rspec/rspecTests.ts b/src/rspec/rspecTestRunner.ts similarity index 97% rename from src/rspec/rspecTests.ts rename to src/rspec/rspecTestRunner.ts index 74f4a42..40bd020 100644 --- a/src/rspec/rspecTests.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; import * as childProcess from 'child_process'; -import { Tests } from '../tests'; +import { TestRunner } from '../testRunner'; -export class RspecTests extends Tests { +export class RspecTestRunner extends TestRunner { /** * Representation of the RSpec test suite as a TestSuiteInfo object. * @@ -53,7 +52,7 @@ export class RspecTests extends Tests { "View error message" ).then(selection => { if (selection === "View error message") { - let outputJson = JSON.parse(Tests.getJsonFromOutput(stdout)); + let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); if (outputJson.messages.length > 0) { diff --git a/src/rspecTests.ts b/src/rspecTests.ts deleted file mode 100644 index 1fb341b..0000000 --- a/src/rspecTests.ts +++ /dev/null @@ -1,289 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import { Tests } from './tests'; - -export class RspecTests extends Tests { - testFrameworkName = 'RSpec'; - - /** - * Representation of the RSpec test suite as a TestSuiteInfo object. - * - * @return The RSpec test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let rspecTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(rspecTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load RSpec tests: ${err.message}`); - return reject(err); - } - } - }); - - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the RSpec JSON formatter. - */ - initTests = async () => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommandWithFilePattern()} --require ${this.getCustomFormatterLocation()}` - + ` --format CustomFormatter --order defined --dry-run`; - - this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192 - }; - - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding RSpec test suite: ${err.message}`); - // Show an error message. - vscode.window.showWarningMessage( - "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", - "View error message" - ).then(selection => { - if (selection === "View error message") { - let outputJson = JSON.parse(Tests.getJsonFromOutput(stdout)); - let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); - - if (outputJson.messages.length > 0) { - let outputJsonString = outputJson.messages.join("\n\n"); - let outputJsonArray = outputJsonString.split("\n"); - outputJsonArray.forEach((line: string) => { - outputChannel.appendLine(line); - }) - } else { - outputChannel.append(err.message); - } - outputChannel.show(false); - } - }); - - throw err; - } - resolve(stdout); - }); - }); - - /** - * Get the user-configured RSpec command, if there is one. - * - * @return The RSpec command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); - return command || `bundle exec rspec` - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` - ); - } - /** - * Get the user-configured RSpec command and add file pattern detection. - * - * @return The RSpec command - */ - 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(',') - command = command || `bundle exec rspec` - return `${command} --pattern '${pattern}'`; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The spec directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); - return directory || './spec/'; - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getCustomFormatterLocation(): string { - return this.context.asAbsolutePath('./custom_formatter.rb'); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` - let cmd = `${this.getTestCommand()} ${args}` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig, args); - } - return cmd - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "EXT_DIR": this.getRubyScriptsLocation(), - }); - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testLocation}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/spec.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Handles test state based on the output returned by the custom RSpec formatter. - * - * @param test The test that we want to handle. - */ - handleStatus(test: any): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); - if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); - } else if (test.status === "failed" && test.pending_message === null) { - // Remove linebreaks from error message. - let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); - // Prepend the class name to the error message string. - let errorMessage: string = `${test.exception.class}:\n${errorMessageNoLinebreaks}`; - - let fileBacktraceLineNumber: number | undefined; - - let filePath = test.file_path.replace('./', ''); - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - // If the backtrace line includes the current file path, try to get the line number from it. - if (line.includes(filePath)) { - let filePathArray = filePath.split('/'); - let fileName = filePathArray[filePathArray.length - 1]; - // Input: spec/models/game_spec.rb:75:in `block (3 levels) in - // Output: 75 - let regex = new RegExp(`${fileName}\:(\\d+)`); - let match = line.match(regex); - if (match && match[1]) { - fileBacktraceLineNumber = parseInt(match[1]); - } - } - }); - } - - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - // Strip line breaks from the message. - message: errorMessageNoLinebreaks, - line: (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1 - }] - }); - } else if (test.status === "failed" && test.pending_message !== null) { - // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); - } - }; -} diff --git a/src/testFactory.ts b/src/testFactory.ts new file mode 100644 index 0000000..ba8c6dd --- /dev/null +++ b/src/testFactory.ts @@ -0,0 +1,103 @@ +import * as vscode from 'vscode'; +import { getTestFramework } from './frameworkDetector'; +import { IVSCodeExtLogger } from "@vscode-logging/logger"; +import { RspecTestLoader } from './rspec/rspecTestLoader'; +import { MinitestTestLoader } from './minitest/minitestTestLoader'; +import { RspecTestRunner } from './rspec/rspecTestRunner'; +import { MinitestTestRunner } from './minitest/minitestTestRunner'; + +export class TestFactory implements vscode.Disposable { + private loader: RspecTestLoader | MinitestTestLoader | null = null; + private runner: RspecTestRunner | MinitestTestRunner | null = null; + protected disposables: { dispose(): void }[] = []; + protected framework: string; + + constructor( + protected readonly log: IVSCodeExtLogger, + protected readonly context: vscode.ExtensionContext, + protected readonly workspace: vscode.WorkspaceFolder | null, + protected readonly controller: vscode.TestController + ) { + this.disposables.push(this.configWatcher()); + this.framework = getTestFramework(this.log); + } + + dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); + } + this.disposables = []; + } + + public getRunner(): RspecTestRunner | MinitestTestRunner { + if (!this.runner) { + this.runner = this.framework == "rspec" + ? new RspecTestRunner( + this.context, + this.log, + this.workspace, + this.controller + ) + : new MinitestTestRunner( + this.context, + this.log, + this.workspace, + this.controller + ) + this.disposables.push(this.runner); + } + return this.runner + } + + public getLoader(): RspecTestLoader | MinitestTestLoader { + if (!this.loader) { + this.loader = this.framework == "rspec" + ? new RspecTestLoader( + this.log, + this.context, + this.workspace, + this.controller, + this.getRunner() + ) + : new MinitestTestLoader( + this.log, + this.context, + this.workspace, + this.controller, + this.getRunner()); + this.disposables.push(this.loader) + } + return this.loader + } + + private configWatcher(): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(configChange => { + this.log.info('Configuration changed'); + if (configChange.affectsConfiguration("rubyTestExplorer")) { + let newFramework = getTestFramework(this.log); + if (newFramework !== this.framework) { + // Config has changed to a different framework - recreate test loader and runner + if (this.loader) { + this.disposeInstance(this.loader) + this.loader = null + } + if (this.runner) { + this.disposeInstance(this.runner) + this.runner = null + } + } + } + }) + } + + private disposeInstance(instance: vscode.Disposable) { + let index = this.disposables.indexOf(instance); + if (index !== -1) { + this.disposables.splice(index) + } + else { + this.log.debug("Factory instance not null but missing from disposables when configuration changed"); + } + instance.dispose() + } +} \ No newline at end of file diff --git a/src/testLoader.ts b/src/testLoader.ts index 8888078..6519d4d 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { IVSCodeExtLogger } from '@vscode-logging/logger'; -import { Tests } from './tests'; -import { RspecTests } from './rspec/rspecTests'; -import { MinitestTests } from './minitest/minitestTests'; +import { TestRunner } from './testRunner'; +import { RspecTestRunner } from './rspec/rspecTestRunner'; +import { MinitestTestRunner } from './minitest/minitestTestRunner'; export abstract class TestLoader implements vscode.Disposable { protected _disposables: { dispose(): void }[] = []; @@ -13,7 +13,7 @@ export abstract class TestLoader implements vscode.Disposable { protected readonly context: vscode.ExtensionContext, protected readonly workspace: vscode.WorkspaceFolder | null, protected readonly controller: vscode.TestController, - protected readonly testRunner: RspecTests | MinitestTests + protected readonly testRunner: RspecTestRunner | MinitestTestRunner ) { this._disposables.push(this.createWatcher()); this._disposables.push(this.configWatcher()); @@ -47,7 +47,7 @@ export abstract class TestLoader implements vscode.Disposable { let output = await this.testRunner.initTests(); this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); this.log.debug(`${output}`); - output = Tests.getJsonFromOutput(output); + output = TestRunner.getJsonFromOutput(output); this.log.debug('Parsing the below JSON:'); this.log.debug(`${output}`); let testMetadata; diff --git a/src/testLoaderFactory.ts b/src/testLoaderFactory.ts deleted file mode 100644 index 83ad1f5..0000000 --- a/src/testLoaderFactory.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as vscode from 'vscode'; -import { TestLoader } from "./testLoader"; -import { getTestFramework } from './frameworkDetector'; -import { IVSCodeExtLogger } from "@vscode-logging/logger"; -import { RspecTestLoader } from './rspec/rspecTestLoader'; -import { MinitestTestLoader } from './minitest/minitestTestLoader'; -import { RspecTests } from './rspec/rspecTests'; -import { MinitestTests } from './minitest/minitestTests'; - -export class TestLoaderFactory implements vscode.Disposable { - private _instance: TestLoader | null = null; - protected disposables: { dispose(): void }[] = []; - - constructor( - protected readonly _log: IVSCodeExtLogger, - protected readonly _context: vscode.ExtensionContext, - protected readonly _workspace: vscode.WorkspaceFolder, - protected readonly _controller: vscode.TestController - ) { - this.disposables.push(this.configWatcher()); - } - - dispose(): void { - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - } - - public getLoader(): TestLoader { - if (this._instance) { - return this._instance; - } - - let framework = getTestFramework(this._log); - switch(framework) { - case "rspec": - return new RspecTestLoader( - this._log, - this._context, - this._workspace, - this._controller, - new RspecTests( - this._context, - this._log, - this._workspace, - this._controller - ) - ); - case "minitest": - return new MinitestTestLoader( - this._log, - this._context, - this._workspace, - this._controller, - new MinitestTests( - this._context, - this._log, - this._workspace, - this._controller - )); - default: - throw `Unknown framework ${framework}` - } - } - - private configWatcher(): vscode.Disposable { - return vscode.workspace.onDidChangeConfiguration(configChange => { - this._log.info('Configuration changed'); - if (configChange.affectsConfiguration("rubyTestExplorer")) { - this._instance?.dispose() - this._instance = null - } - }) - } -} \ No newline at end of file diff --git a/src/testRunner.ts b/src/testRunner.ts new file mode 100644 index 0000000..a474f1a --- /dev/null +++ b/src/testRunner.ts @@ -0,0 +1,323 @@ +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; +import * as split2 from 'split2'; +import { IVSCodeExtLogger } from '@vscode-logging/logger'; +import { __asyncDelegator } from 'tslib'; + +export abstract class TestRunner implements vscode.Disposable { + protected currentChildProcess: childProcess.ChildProcess | undefined; + protected testSuite: vscode.TestItem[] | undefined; + abstract testFrameworkName: string; + protected debugCommandStartedResolver: Function | undefined; + + /** + * @param context Extension context provided by vscode. + * @param testStatesEmitter An emitter for the test suite's state. + * @param log The Test Adapter logger, for logging. + */ + constructor( + protected context: vscode.ExtensionContext, + protected log: IVSCodeExtLogger, + protected workspace: vscode.WorkspaceFolder | null, + protected controller: vscode.TestController + ) {} + + abstract tests: () => Promise; + + abstract initTests: () => Promise; + + public dispose() { + this.killChild(); + } + + /** + * Kills the current child process if one exists. + */ + public killChild(): void { + if (this.currentChildProcess) { + this.currentChildProcess.kill(); + } + } + + /** + * Get the user-configured test file pattern. + * + * @return The file pattern + */ + getFilePattern(): Array { + let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); + return pattern || ['*_test.rb', 'test_*.rb']; + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The test directory + */ + abstract getTestDirectory(): string; + + /** + * Pull JSON out of the test framework output. + * + * RSpec and Minitest frequently return bad data even when they're told to + * format the output as JSON, e.g. due to code coverage messages and other + * injections from gems. This gets the JSON by searching for + * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing + * curly brace and `END_OF_TEST_JSON`. These are output by the custom + * RSpec formatter or Minitest Rake task as part of the final JSON output. + * + * @param output The output returned by running a command. + * @return A string representation of the JSON found in the output. + */ + static getJsonFromOutput(output: string): string { + output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); + // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. + return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); + } + + /** + * Get the location of the test in the testing tree. + * + * Test ids are in the form of `/spec/model/game_spec.rb[1:1:1]`, and this + * function turns that into `111`. The number is used to order the tests + * in the explorer. + * + * @param test The test we want to get the location of. + * @return A number representing the location of the test in the test tree. + */ + protected getTestLocation(test: vscode.TestItem): number { + return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); + } + + // /** + // * Sorts an array of TestSuiteInfo objects by label. + // * + // * @param testSuiteChildren An array of TestSuiteInfo objects, generally the children of another TestSuiteInfo object. + // * @return The input array, sorted by label. + // */ + // protected sortTestSuiteChildren(testSuiteChildren: Array): Array { + // testSuiteChildren = testSuiteChildren.sort((a: TestSuiteInfo, b: TestSuiteInfo) => { + // let comparison = 0; + // if (a.label > b.label) { + // comparison = 1; + // } else if (a.label < b.label) { + // comparison = -1; + // } + // return comparison; + // }); + + // return testSuiteChildren; + // } + + /** + * Assigns the process to currentChildProcess and handles its output and what happens when it exits. + * + * @param process A process running the tests. + * @return A promise that resolves when the test run completes. + */ + handleChildProcess = async (process: childProcess.ChildProcess) => new Promise((resolve, reject) => { + this.currentChildProcess = process; + + this.currentChildProcess.on('exit', () => { + this.log.info('Child process has exited. Sending test run finish event.'); + this.currentChildProcess = undefined; + // this.testStatesEmitter.fire({ type: 'finished' }); + resolve('{}'); + }); + + this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { + data = data.toString(); + this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); + if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { + this.debugCommandStartedResolver() + } + }); + + this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { + data = data.toString(); + this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); + if (data.startsWith('PASSED:')) { + data = data.replace('PASSED: ', ''); + // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'passed' }); + } else if (data.startsWith('FAILED:')) { + data = data.replace('FAILED: ', ''); + // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'failed' }); + } else if (data.startsWith('RUNNING:')) { + data = data.replace('RUNNING: ', ''); + // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'running' }); + } else if (data.startsWith('PENDING:')) { + data = data.replace('PENDING: ', ''); + // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'skipped' }); + } + if (data.includes('START_OF_TEST_JSON')) { + resolve(data); + } + }); + }); + + public async runHandler(request: vscode.TestRunRequest, token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration) { + const run = this.controller.createTestRun(request); + const queue: vscode.TestItem[] = []; + + // Loop through all included tests, or all known tests, and add them to our queue + if (request.include) { + request.include.forEach(test => queue.push(test)); + + // For every test that was queued, try to run it. Call run.passed() or run.failed() + while (queue.length > 0 && !token.isCancellationRequested) { + const test = queue.pop()!; + + // Skip tests the user asked to exclude + if (request.exclude?.includes(test)) { + continue; + } + + await this.runNode(test, token, run, debuggerConfig); + + test.children.forEach(test => queue.push(test)); + } + if (token.isCancellationRequested) { + this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + } + } else { + await this.runNode(null, token, run, debuggerConfig); + } + + // Make sure to end the run after all tests have been executed: + run.end(); + } + + /** + * Recursively run a node or its children. + * + * @param node A test or test suite. + * @param debuggerConfig A VS Code debugger configuration. + */ + protected async runNode( + node: vscode.TestItem | null, + token: vscode.CancellationToken, + testRun: vscode.TestRun, + debuggerConfig?: vscode.DebugConfiguration + ): Promise { + // Special case handling for the root suite, since it can be run + // with runFullTestSuite() + if (node == null) { + //this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); + testRun.enqueued + let testOutput = await this.runFullTestSuite(token, debuggerConfig); + testOutput = TestRunner.getJsonFromOutput(testOutput); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; + + if (tests && tests.length > 0) { + tests.forEach((test: { id: string; }) => { + this.handleStatus(test, testRun); + }); + } + // If the suite is a file, run the tests as a file rather than as separate tests. + } else if (node.label.endsWith('.rb')) { + // Mark selected tests as enqueued + this.enqueTestAndChildren(node, testRun) + + testRun.started(node) + let testOutput = await this.runTestFile(token, `${node.uri?.fsPath}`, debuggerConfig); + + testOutput = TestRunner.getJsonFromOutput(testOutput); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; + + if (tests && tests.length > 0) { + tests.forEach((test: { id: string }) => { + this.handleStatus(test, testRun); + }); + } + + if (tests.length != node.children.size + 1) { + this.log.debug(`Test count mismatch {${node.label}}. Expected ${node.children.size + 1}, ran ${tests.length}`) + } + + //this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); + + } else { + if (node.uri !== undefined && node.range !== undefined) { + testRun.started(node) + + // Run the test at the given line, add one since the line is 0-indexed in + // VS Code and 1-indexed for RSpec/Minitest. + let testOutput = await this.runSingleTest(token, `${node.uri.fsPath}:${node.range?.end.line}`, debuggerConfig); + + testOutput = TestRunner.getJsonFromOutput(testOutput); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let currentTest = testMetadata.examples[0]; + + this.handleStatus(currentTest, testRun); + } + } + } + + public async debugCommandStarted(): Promise { + return new Promise(async (resolve, reject) => { + this.debugCommandStartedResolver = resolve; + setTimeout(() => { reject("debugCommandStarted timed out") }, 10000) + }) + } + + /** + * Get the absolute path of the custom_formatter.rb file. + * + * @return The spec directory + */ + protected getRubyScriptsLocation(): string { + return vscode.Uri.joinPath(this.context.extensionUri, 'ruby').fsPath; + } + + /** + * Mark a test node and all its children as being queued for execution + */ + private enqueTestAndChildren(test: vscode.TestItem, testRun: vscode.TestRun) { + testRun.enqueued(test) + if (test.children && test.children.size > 0) { + test.children.forEach(child => { this.enqueTestAndChildren(child, testRun) }) + } + } + + /** + * Runs a single test. + * + * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test. + */ + abstract runSingleTest: (token: vscode.CancellationToken, testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; + + /** + * Runs tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the tests. + */ + abstract runTestFile: (token: vscode.CancellationToken, testFile: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; + + /** + * Runs the full test suite for the current workspace. + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The raw output from running the test suite. + */ + abstract runFullTestSuite: (token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration) => Promise; + + /** + * Handles test state based on the output returned by the test command. + * + * @param test The test that we want to handle. + * @param testRun Test run object for reporting test result + */ + abstract handleStatus(test: any, testRun: vscode.TestRun): void; +} diff --git a/src/tests.ts b/src/tests.ts deleted file mode 100644 index ce29bd3..0000000 --- a/src/tests.ts +++ /dev/null @@ -1,554 +0,0 @@ -import * as vscode from 'vscode'; -import { TestSuiteInfo, TestInfo, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; -import * as childProcess from 'child_process'; -import * as split2 from 'split2'; -import { Log } from 'vscode-test-adapter-util'; - -export abstract class Tests { - protected context: vscode.ExtensionContext; - protected testStatesEmitter: vscode.EventEmitter; - protected currentChildProcess: childProcess.ChildProcess | undefined; - protected log: Log; - protected testSuite: TestSuiteInfo | undefined; - protected workspace: vscode.WorkspaceFolder; - abstract testFrameworkName: string; - protected debugCommandStartedResolver: Function | undefined; - - /** - * @param context Extension context provided by vscode. - * @param testStatesEmitter An emitter for the test suite's state. - * @param log The Test Adapter logger, for logging. - */ - constructor( - context: vscode.ExtensionContext, - testStatesEmitter: vscode.EventEmitter, - log: Log, - workspace: vscode.WorkspaceFolder - ) { - this.context = context; - this.testStatesEmitter = testStatesEmitter; - this.log = log; - this.workspace = workspace; - } - - abstract tests: () => Promise; - - abstract initTests: () => Promise; - - /** - * Takes the output from initTests() and parses the resulting - * JSON into a TestSuiteInfo object. - * - * @return The full test suite. - */ - public async loadTests(): Promise { - let output = await this.initTests(); - this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); - this.log.debug(`${output}`); - output = Tests.getJsonFromOutput(output); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${output}`); - let testMetadata; - try { - testMetadata = JSON.parse(output); - } catch (error) { - this.log.error(`JSON parsing failed: ${error}`); - } - - let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; - - testMetadata.examples.forEach((test: { id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }) => { - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); - tests.push(test); - }); - - let testSuite: TestSuiteInfo = await this.getBaseTestSuite(tests); - - // Sort the children of each test suite based on their location in the test tree. - (testSuite.children as Array).forEach((suite: TestSuiteInfo) => { - // NOTE: This will only sort correctly if everything is nested at the same - // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is - // introduced, the location is generated as e.g. 1231, which won't - // sort properly relative to everything else. - (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { - if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { - let aLocation: number = this.getTestLocation(a as TestInfo); - let bLocation: number = this.getTestLocation(b as TestInfo); - return aLocation - bLocation; - } else { - return 0; - } - }) - }); - - this.testSuite = testSuite; - - return Promise.resolve(testSuite); - } - - /** - * Kills the current child process if one exists. - */ - public killChild(): void { - if (this.currentChildProcess) { - this.currentChildProcess.kill(); - } - } - - /** - * Get the user-configured test file pattern. - * - * @return The file pattern - */ - getFilePattern(): Array { - let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); - return pattern || ['*_test.rb', 'test_*.rb']; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - abstract getTestDirectory(): string; - - /** - * Pull JSON out of the test framework output. - * - * RSpec and Minitest frequently return bad data even when they're told to - * format the output as JSON, e.g. due to code coverage messages and other - * injections from gems. This gets the JSON by searching for - * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing - * curly brace and `END_OF_TEST_JSON`. These are output by the custom - * RSpec formatter or Minitest Rake task as part of the final JSON output. - * - * @param output The output returned by running a command. - * @return A string representation of the JSON found in the output. - */ - static getJsonFromOutput(output: string): string { - output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); - // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. - return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); - } - - /** - * Get the location of the test in the testing tree. - * - * Test ids are in the form of `/spec/model/game_spec.rb[1:1:1]`, and this - * function turns that into `111`. The number is used to order the tests - * in the explorer. - * - * @param test The test we want to get the location of. - * @return A number representing the location of the test in the test tree. - */ - protected getTestLocation(test: TestInfo): number { - return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); - } - - /** - * Convert a string from snake_case to PascalCase. - * Note that the function will return the input string unchanged if it - * includes a '/'. - * - * @param string The string to convert to PascalCase. - * @return The converted string. - */ - protected snakeToPascalCase(string: string): string { - if (string.includes('/')) { return string } - return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); - } - - /** - * Sorts an array of TestSuiteInfo objects by label. - * - * @param testSuiteChildren An array of TestSuiteInfo objects, generally the children of another TestSuiteInfo object. - * @return The input array, sorted by label. - */ - protected sortTestSuiteChildren(testSuiteChildren: Array): Array { - testSuiteChildren = testSuiteChildren.sort((a: TestSuiteInfo, b: TestSuiteInfo) => { - let comparison = 0; - if (a.label > b.label) { - comparison = 1; - } else if (a.label < b.label) { - comparison = -1; - } - return comparison; - }); - - return testSuiteChildren; - } - - /** - * Get the tests in a given file. - */ - public getTestSuiteForFile( - { tests, currentFile, directory }: { - tests: Array<{ - id: string; - full_description: string; - description: string; - file_path: string; - line_number: number; - location: number; - }>; currentFile: string; directory?: string; - }): TestSuiteInfo { - let currentFileTests = tests.filter(test => { - return test.file_path === currentFile - }); - - let currentFileTestsInfo = currentFileTests as unknown as Array; - currentFileTestsInfo.forEach((test: TestInfo) => { - test.type = 'test'; - test.label = ''; - }); - - let currentFileLabel = ''; - - if (directory) { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}${directory}/`, ''); - } else { - currentFileLabel = currentFile.replace(`${this.getTestDirectory()}`, ''); - } - - let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - - let currentFileTestInfoArray: Array = currentFileTests.map((test) => { - // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", - // but with the latter's first character stripped. - let filePath: string = `${this.workspace.uri.fsPath}${test.file_path.substr(1)}`; - - // RSpec provides test ids like "file_name.rb[1:2:3]". - // This uses the digits at the end of the id to create - // an array of numbers representing the location of the - // test in the file. - let testLocationArray: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').map((x) => { - return parseInt(x); - }); - - // Get the last element in the location array. - let testNumber: number = testLocationArray[testLocationArray.length - 1]; - // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" - // is appended to the test description to distinguish between separate tests. - let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; - - // If the current file label doesn't have a slash in it and it starts with the PascalCase'd - // file name, remove the from the start of the description. This turns, e.g. - // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. - if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { - // Optional check for a space following the PascalCase file name. In some - // cases, e.g. 'FileName#method_name` there's no space after the file name. - let regexString = `${pascalCurrentFileLabel}[ ]?`; - let regex = new RegExp(regexString, "g"); - description = description.replace(regex, ''); - } - - let testInfo: TestInfo = { - type: 'test', - id: test.id, - label: description, - file: filePath, - // Line numbers are 0-indexed - line: test.line_number - 1 - } - - return testInfo; - }); - - let currentFileAsAbsolutePath = `${this.workspace.uri.fsPath}${currentFile.substr(1)}`; - - let currentFileTestSuite: TestSuiteInfo = { - type: 'suite', - id: currentFile, - label: currentFileLabel, - file: currentFileAsAbsolutePath, - children: currentFileTestInfoArray - } - - return currentFileTestSuite; - } - - /** - * Create the base test suite with a root node and one layer of child nodes - * representing the subdirectories of spec/, and then any files under the - * given subdirectory. - * - * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. - * @return The test suite root with its children. - */ - public async getBaseTestSuite( - tests: any[] - ): Promise { - let rootTestSuite: TestSuiteInfo = { - type: 'suite', - id: 'root', - label: `${this.workspace.name} ${this.testFrameworkName}`, - children: [] - }; - - // Create an array of all test files and then abuse Sets to make it unique. - let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; - - let splitFilesArray: Array = []; - - // Remove the spec/ directory from all the file path. - uniqueFiles.forEach((file) => { - splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); - }); - - // This gets the main types of tests, e.g. features, helpers, models, requests, etc. - let subdirectories: Array = []; - splitFilesArray.forEach((splitFile) => { - if (splitFile.length > 1) { - subdirectories.push(splitFile[0]); - } - }); - subdirectories = [...new Set(subdirectories)]; - - // A nested loop to iterate through the direct subdirectories of spec/ and then - // organize the files under those subdirectories. - subdirectories.forEach((directory) => { - let filesInDirectory: Array = []; - - let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - return file.startsWith(`${this.getTestDirectory()}${directory}/`); - }); - - // Get the sets of tests for each file in the current directory. - uniqueFilesInDirectory.forEach((currentFile: string) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile, directory }); - filesInDirectory.push(currentFileTestSuite); - }); - - let directoryTestSuite: TestSuiteInfo = { - type: 'suite', - id: directory, - label: directory, - children: filesInDirectory - }; - - rootTestSuite.children.push(directoryTestSuite); - }); - - // Sort test suite types alphabetically. - rootTestSuite.children = this.sortTestSuiteChildren(rootTestSuite.children as Array); - - // Get files that are direct descendants of the spec/ directory. - let topDirectoryFiles = uniqueFiles.filter((filePath) => { - return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; - }); - - topDirectoryFiles.forEach((currentFile) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); - rootTestSuite.children.push(currentFileTestSuite); - }); - - return rootTestSuite; - } - - /** - * Assigns the process to currentChildProcess and handles its output and what happens when it exits. - * - * @param process A process running the tests. - * @return A promise that resolves when the test run completes. - */ - handleChildProcess = async (process: childProcess.ChildProcess) => new Promise((resolve, reject) => { - this.currentChildProcess = process; - - this.currentChildProcess.on('exit', () => { - this.log.info('Child process has exited. Sending test run finish event.'); - this.currentChildProcess = undefined; - this.testStatesEmitter.fire({ type: 'finished' }); - resolve('{}'); - }); - - this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { - data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); - if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { - this.debugCommandStartedResolver() - } - }); - - this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { - data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); - if (data.startsWith('PASSED:')) { - data = data.replace('PASSED: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'passed' }); - } else if (data.startsWith('FAILED:')) { - data = data.replace('FAILED: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'failed' }); - } else if (data.startsWith('RUNNING:')) { - data = data.replace('RUNNING: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'running' }); - } else if (data.startsWith('PENDING:')) { - data = data.replace('PENDING: ', ''); - this.testStatesEmitter.fire({ type: 'test', test: data, state: 'skipped' }); - } - if (data.includes('START_OF_TEST_JSON')) { - resolve(data); - } - }); - }); - - /** - * Runs the test suite by iterating through each test and running it. - * - * @param tests - * @param debuggerConfig A VS Code debugger configuration. - */ - runTests = async (tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise => { - let testSuite: TestSuiteInfo = await this.tests(); - - for (const suiteOrTestId of tests) { - const node = this.findNode(testSuite, suiteOrTestId); - if (node) { - await this.runNode(node, debuggerConfig); - } - } - } - - /** - * Recursively search for a node in the test suite list. - * - * @param searchNode The test or test suite to search in. - * @param id The id of the test or test suite. - */ - protected findNode(searchNode: TestSuiteInfo | TestInfo, id: string): TestSuiteInfo | TestInfo | undefined { - if (searchNode.id === id) { - return searchNode; - } else if (searchNode.type === 'suite') { - for (const child of searchNode.children) { - const found = this.findNode(child, id); - if (found) return found; - } - } - return undefined; - } - - /** - * Recursively run a node or its children. - * - * @param node A test or test suite. - * @param debuggerConfig A VS Code debugger configuration. - */ - protected async runNode(node: TestSuiteInfo | TestInfo, debuggerConfig?: vscode.DebugConfiguration): Promise { - // Special case handling for the root suite, since it can be run - // with runFullTestSuite() - if (node.type === 'suite' && node.id === 'root') { - this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); - - let testOutput = await this.runFullTestSuite(debuggerConfig); - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string | TestInfo; }) => { - this.handleStatus(test); - }); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - // If the suite is a file, run the tests as a file rather than as separate tests. - } else if (node.type === 'suite' && node.label.endsWith('.rb')) { - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); - - let testOutput = await this.runTestFile(`${node.file}`, debuggerConfig); - - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string | TestInfo; }) => { - this.handleStatus(test); - }); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - - } else if (node.type === 'suite') { - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'running' }); - - for (const child of node.children) { - await this.runNode(child, debuggerConfig); - } - - this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - - } else if (node.type === 'test') { - if (node.file !== undefined && node.line !== undefined) { - this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); - - // Run the test at the given line, add one since the line is 0-indexed in - // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(`${node.file}:${node.line + 1}`, debuggerConfig); - - testOutput = Tests.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let currentTest = testMetadata.examples[0]; - - this.handleStatus(currentTest); - } - } - } - - public async debugCommandStarted(): Promise { - return new Promise(async (resolve, reject) => { - this.debugCommandStartedResolver = resolve; - setTimeout(() => { reject("debugCommandStarted timed out") }, 10000) - }) - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return this.context.asAbsolutePath('./ruby'); - } - - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - abstract runSingleTest: (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - abstract runTestFile: (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - abstract runFullTestSuite: (debuggerConfig?: vscode.DebugConfiguration) => Promise; - - /** - * Handles test state based on the output returned by the test command. - * - * @param test The test that we want to handle. - */ - abstract handleStatus(test: any): void; -} From fde28f279f9488714755ec2b1f868378f073d122 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 05:31:48 +0000 Subject: [PATCH 004/108] Finish converting the test runners --- src/main.ts | 7 +- src/minitest/minitestTestRunner.ts | 138 ++++--------------- src/rspec/rspecTestRunner.ts | 132 ++++--------------- src/testLoader.ts | 10 +- src/testRunContext.ts | 158 ++++++++++++++++++++++ src/testRunner.ts | 205 +++++++++++++++++++++++------ 6 files changed, 388 insertions(+), 262 deletions(-) create mode 100644 src/testRunContext.ts diff --git a/src/main.ts b/src/main.ts index 1223c19..a552ad8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,12 +39,13 @@ export async function activate(context: vscode.ExtensionContext) { testLoaderFactory.getLoader().loadAllTests(); + // TODO: Allow lazy-loading of child tests - below is taken from example in docs // Custom handler for loading tests. The "test" argument here is undefined, // but if we supported lazy-loading child test then this could be called with // the test whose children VS Code wanted to load. - controller.resolveHandler = test => { - controller.items.replace([]); // TODO: Load tests - }; + // controller.resolveHandler = test => { + // controller.items.replace([]); + // }; // TODO: (?) Add a "Profile" profile for profiling tests controller.createRunProfile( diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 89f2c83..6ab9439 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -1,28 +1,11 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; +import { TestRunContext } from '../testRunContext'; export class MinitestTestRunner extends TestRunner { testFrameworkName = 'Minitest'; - /** - * Representation of the Minitest test suite as a TestSuiteInfo object. - * - * @return The Minitest test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let minitestTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(minitestTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load Minitest tests: ${err.message}`); - return reject(err); - } - } - }); - /** * Perform a dry-run of the test suite to get information about every test. * @@ -33,7 +16,7 @@ export class MinitestTestRunner extends TestRunner { // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, + cwd: this.workspace?.uri.fsPath, maxBuffer: 8192 * 8192, env: this.getProcessEnv() }; @@ -126,98 +109,32 @@ export class MinitestTestRunner extends TestRunner { return cmd; } - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); + protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { let line = testLocation.split(':').pop(); - let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace.uri.fsPath}/`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeLocation}:${line}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - let relativeFile = testFile.replace(`${this.workspace.uri.fsPath}/`, "").replace(`./`, "") - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithDebugger(debuggerConfig)} '${relativeFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = this.testCommandWithDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); + let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace?.uri.fsPath || "."}/`, "") + return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeLocation}:${line}'` + }; - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); + protected getTestFileCommand(testFile: string, context: TestRunContext): string { + let relativeFile = testFile.replace(`${this.workspace?.uri.fsPath || '.'}/`, "").replace(`./`, "") + return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeFile}'` + }; - resolve(await this.handleChildProcess(testProcess)); - }); + protected getFullTestSuiteCommand(context: TestRunContext): string { + return this.testCommandWithDebugger(context.debuggerConfig) + }; /** * Handles test state based on the output returned by the Minitest Rake task. * * @param test The test that we want to handle. + * @param context Test run context */ - handleStatus(test: any): void { + handleStatus(test: any, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); + context.passed(test.id) } else if (test.status === "failed" && test.pending_message === null) { - let errorMessageShort: string = test.exception.message; let errorMessageLine: number = test.line_number; let errorMessage: string = test.exception.message; @@ -237,19 +154,20 @@ export class MinitestTestRunner extends TestRunner { }); } - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - message: errorMessageShort, - line: errorMessageLine - 1 - }] - }); + context.failed( + test.id, + errorMessage, + test.file_path.replace('./', ''), + errorMessageLine - 1 + ) } else if (test.status === "failed" && test.pending_message !== null) { // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); + context.errored( + test.id, + test.pending_message, + test.file_path.replace('./', ''), + test.line_number + ) } }; } diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 40bd020..2a55046 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,26 +1,9 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; +import { TestRunContext } from '../testRunContext'; export class RspecTestRunner extends TestRunner { - /** - * Representation of the RSpec test suite as a TestSuiteInfo object. - * - * @return The RSpec test suite as a TestSuiteInfo object. - */ - tests = async () => new Promise((resolve, reject) => { - try { - // If test suite already exists, use testSuite. Otherwise, load them. - let rspecTests = this.testSuite ? this.testSuite : this.loadTests(); - return resolve(rspecTests); - } catch (err) { - if (err instanceof Error) { - this.log.error(`Error while attempting to load RSpec tests: ${err.message}`); - return reject(err); - } - } - }); - /** * Perform a dry-run of the test suite to get information about every test. * @@ -39,7 +22,7 @@ export class RspecTestRunner extends TestRunner { // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, + cwd: this.workspace?.uri.fsPath, maxBuffer: 8192 * 8192, }; @@ -157,91 +140,28 @@ export class RspecTestRunner extends TestRunner { }); } - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/spec.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test. - */ - runSingleTest = async (testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running single test: ${testLocation}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true, - env: this.getProcessEnv() - }; - - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testLocation}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/spec.rb`. - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the tests. - */ - runTestFile = async (testFile: string, debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running test file: ${testFile}`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - // Run tests for a given file at once with a single command. - let testCommand = `${this.testCommandWithFormatterAndDebugger(debuggerConfig)} '${testFile}'`; - this.log.info(`Running command: ${testCommand}`); - - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); - - resolve(await this.handleChildProcess(testProcess)); - }); - - /** - * Runs the full test suite for the current workspace. - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The raw output from running the test suite. - */ - runFullTestSuite = async (debuggerConfig?: vscode.DebugConfiguration) => new Promise(async (resolve, reject) => { - this.log.info(`Running full test suite.`); - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace.uri.fsPath, - shell: true - }; - - let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); - this.log.info(`Running command: ${testCommand}`); + protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { + return `${this.testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testLocation}'` + }; - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); + protected getTestFileCommand(testFile: string, context: TestRunContext): string { + return `${this.testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testFile}'` + }; - resolve(await this.handleChildProcess(testProcess)); - }); + protected getFullTestSuiteCommand(context: TestRunContext): string { + return this.testCommandWithFormatterAndDebugger(context.debuggerConfig) + }; /** * Handles test state based on the output returned by the custom RSpec formatter. * * @param test The test that we want to handle. + * @param context Test run context */ - handleStatus(test: any): void { + handleStatus(test: any, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); if (test.status === "passed") { - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'passed' }); + context.passed(test.id) } else if (test.status === "failed" && test.pending_message === null) { // Remove linebreaks from error message. let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); @@ -272,20 +192,20 @@ export class RspecTestRunner extends TestRunner { }); } - this.testStatesEmitter.fire({ - type: 'test', - test: test.id, - state: 'failed', - message: errorMessage, - decorations: [{ - // Strip line breaks from the message. - message: errorMessageNoLinebreaks, - line: (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1 - }] - }); + context.failed( + test.id, + errorMessage, + filePath, + (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, + ) } else if (test.status === "failed" && test.pending_message !== null) { // Handle pending test cases. - this.testStatesEmitter.fire({ type: 'test', test: test.id, state: 'skipped', message: test.pending_message }); + context.errored( + test.id, + test.pending_message, + test.file_path.replace('./', ''), + test.line_number + ) } }; } diff --git a/src/testLoader.ts b/src/testLoader.ts index 6519d4d..5125ae5 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -6,7 +6,7 @@ import { RspecTestRunner } from './rspec/rspecTestRunner'; import { MinitestTestRunner } from './minitest/minitestTestRunner'; export abstract class TestLoader implements vscode.Disposable { - protected _disposables: { dispose(): void }[] = []; + protected disposables: { dispose(): void }[] = []; constructor( protected readonly log: IVSCodeExtLogger, @@ -15,15 +15,15 @@ export abstract class TestLoader implements vscode.Disposable { protected readonly controller: vscode.TestController, protected readonly testRunner: RspecTestRunner | MinitestTestRunner ) { - this._disposables.push(this.createWatcher()); - this._disposables.push(this.configWatcher()); + this.disposables.push(this.createWatcher()); + this.disposables.push(this.configWatcher()); } dispose(): void { - for (const disposable of this._disposables) { + for (const disposable of this.disposables) { disposable.dispose(); } - this._disposables = []; + this.disposables = []; } /** diff --git a/src/testRunContext.ts b/src/testRunContext.ts new file mode 100644 index 0000000..c6ca62f --- /dev/null +++ b/src/testRunContext.ts @@ -0,0 +1,158 @@ +import * as vscode from 'vscode' +import { IVSCodeExtLogger } from '@vscode-logging/logger' + +/** + * Test run context + * + * Contains all objects used for interacting with VS Test API while tests are running + */ +export class TestRunContext { + public readonly testRun: vscode.TestRun + + /** + * Create a new context + * + * @param log Logger + * @param token Cancellation token triggered when the user cancels a test operation + * @param request Test run request for creating test run object + * @param controller Test controller to look up tests for status reporting + * @param debuggerConfig A VS Code debugger configuration. + */ + constructor( + public readonly log: IVSCodeExtLogger, + public readonly token: vscode.CancellationToken, + request: vscode.TestRunRequest, + private readonly controller: vscode.TestController, + public readonly debuggerConfig?: vscode.DebugConfiguration + ) { + this.testRun = controller.createTestRun(request) + } + + /** + * Indicates a test is queued for later execution. + * + * @param testId ID of the test item to update. + */ + public enqueued(test: string | vscode.TestItem): void { + if (typeof test === "string") { + this.log.debug(`Enqueued: ${test}`) + this.testRun.enqueued(this.getTestItem(test)) + } + else { + this.log.debug(`Enqueued: ${test.id}`) + this.testRun.enqueued(test) + } + } + + /** + * Indicates a test has errored. + * + * This differs from the "failed" state in that it indicates a test that couldn't be executed at all, from a compilation error for example + * + * @param testId ID of the test item to update. + * @param message Message(s) associated with the test failure. + * @param duration How long the test took to execute, in milliseconds. + */ + public errored( + testId: string, + message: string, + file: string, + line: number, + duration?: number | undefined + ): void { + this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + let testMessage = new vscode.TestMessage(message) + let testItem = this.getTestItem(testId) + testMessage.location = new vscode.Location( + testItem.uri ?? vscode.Uri.file(file), + new vscode.Position(line, 0) + ) + this.testRun.errored(testItem, testMessage, duration) + } + + /** + * Indicates a test has failed. + * + * @param testId ID of the test item to update. + * @param message Message(s) associated with the test failure. + * @param file Path to the file containing the failed test + * @param line Line number where the error occurred + * @param duration How long the test took to execute, in milliseconds. + */ + public failed( + testId: string, + message: string, + file: string, + line: number, + duration?: number | undefined + ): void { + this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + let testMessage = new vscode.TestMessage(message) + let testItem = this.getTestItem(testId) + testMessage.location = new vscode.Location( + testItem.uri ?? vscode.Uri.file(file), + new vscode.Position(line, 0) + ) + this.testRun.failed(testItem, testMessage, duration) + } + + /** + * Indicates a test has passed. + * + * @param testId ID of the test item to update. + * @param duration How long the test took to execute, in milliseconds. + */ + public passed( + testId: string, + duration?: number | undefined + ): void { + this.log.debug(`Passed: ${testId}${duration ? `, duration: ${duration}ms` : ''}`) + this.testRun.passed(this.getTestItem(testId), duration) + } + + /** + * Indicates a test has been skipped. + * + * @param test ID of the test item to update, or the test item. + */ + public skipped(test: string | vscode.TestItem): void { + if (typeof test === "string") { + this.log.debug(`Skipped: ${test}`) + this.testRun.skipped(this.getTestItem(test)) + } + else { + this.log.debug(`Skipped: ${test.id}`) + this.testRun.skipped(test) + } + } + + /** + * Indicates a test has started running. + * + * @param testId ID of the test item to update, or the test item. + */ + public started(test: string | vscode.TestItem): void { + if (typeof test === "string") { + this.log.debug(`Started: ${test}`) + this.testRun.started(this.getTestItem(test)) + } + else { + this.log.debug(`Started: ${test.id}`) + this.testRun.started(test) + } + } + + /** + * Get the {@link vscode.TestItem} for a test ID + * @param testId Test ID to lookup + * @returns The test item for the ID + * @throws if test item could not be found + */ + public getTestItem(testId: string): vscode.TestItem { + let testItem = this.controller.items.get(testId) + if (!testItem) { + throw `Test not found on controller: ${testId}` + } + return testItem + } +} \ No newline at end of file diff --git a/src/testRunner.ts b/src/testRunner.ts index a474f1a..1d4bf71 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -3,12 +3,13 @@ import * as childProcess from 'child_process'; import * as split2 from 'split2'; import { IVSCodeExtLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; +import { TestRunContext } from './testRunContext'; export abstract class TestRunner implements vscode.Disposable { protected currentChildProcess: childProcess.ChildProcess | undefined; protected testSuite: vscode.TestItem[] | undefined; - abstract testFrameworkName: string; protected debugCommandStartedResolver: Function | undefined; + protected disposables: { dispose(): void }[] = []; /** * @param context Extension context provided by vscode. @@ -22,12 +23,25 @@ export abstract class TestRunner implements vscode.Disposable { protected controller: vscode.TestController ) {} - abstract tests: () => Promise; + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + protected abstract getProcessEnv(): any + /** + * Initialise the test framework, parse tests (without executing) and retrieve the output + * @return Stdout outpu from framework initialisation + */ abstract initTests: () => Promise; public dispose() { this.killChild(); + for (const disposable of this.disposables) { + disposable.dispose(); + } + this.disposables = []; } /** @@ -115,13 +129,13 @@ export abstract class TestRunner implements vscode.Disposable { * @param process A process running the tests. * @return A promise that resolves when the test run completes. */ - handleChildProcess = async (process: childProcess.ChildProcess) => new Promise((resolve, reject) => { + handleChildProcess = async (process: childProcess.ChildProcess, context: TestRunContext) => new Promise((resolve, reject) => { this.currentChildProcess = process; this.currentChildProcess.on('exit', () => { this.log.info('Child process has exited. Sending test run finish event.'); this.currentChildProcess = undefined; - // this.testStatesEmitter.fire({ type: 'finished' }); + context.testRun.end() resolve('{}'); }); @@ -133,21 +147,22 @@ export abstract class TestRunner implements vscode.Disposable { } }); + // TODO: Parse test IDs, durations, and failure message(s) from data this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); if (data.startsWith('PASSED:')) { data = data.replace('PASSED: ', ''); - // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'passed' }); + context.passed(data) } else if (data.startsWith('FAILED:')) { data = data.replace('FAILED: ', ''); - // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'failed' }); + context.failed(data, "", "", 0) } else if (data.startsWith('RUNNING:')) { data = data.replace('RUNNING: ', ''); - // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'running' }); + context.started(data) } else if (data.startsWith('PENDING:')) { data = data.replace('PENDING: ', ''); - // this.testStatesEmitter.fire({ type: 'test', test: data, state: 'skipped' }); + context.enqueued(data) } if (data.includes('START_OF_TEST_JSON')) { resolve(data); @@ -155,8 +170,26 @@ export abstract class TestRunner implements vscode.Disposable { }); }); - public async runHandler(request: vscode.TestRunRequest, token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration) { - const run = this.controller.createTestRun(request); + /** + * Test run handler + * + * Called by VSC when a user requests a test run + * @param request Request containing tests to be run and tests to be excluded from the run + * @param token Cancellation token which will trigger when a user cancels a test run + * @param debuggerConfig VSC Debugger configuration if a debug run was requested, or `null` + */ + public async runHandler( + request: vscode.TestRunRequest, + token: vscode.CancellationToken, + debuggerConfig?: vscode.DebugConfiguration + ) { + const context = new TestRunContext( + this.log, + token, + request, + this.controller, + debuggerConfig + ) const queue: vscode.TestItem[] = []; // Loop through all included tests, or all known tests, and add them to our queue @@ -172,7 +205,7 @@ export abstract class TestRunner implements vscode.Disposable { continue; } - await this.runNode(test, token, run, debuggerConfig); + await this.runNode(test, context); test.children.forEach(test => queue.push(test)); } @@ -180,31 +213,31 @@ export abstract class TestRunner implements vscode.Disposable { this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } else { - await this.runNode(null, token, run, debuggerConfig); + await this.runNode(null, context); } // Make sure to end the run after all tests have been executed: - run.end(); + context.testRun.end(); } /** * Recursively run a node or its children. * * @param node A test or test suite. - * @param debuggerConfig A VS Code debugger configuration. + * @param context Test run context */ protected async runNode( node: vscode.TestItem | null, - token: vscode.CancellationToken, - testRun: vscode.TestRun, - debuggerConfig?: vscode.DebugConfiguration + context: TestRunContext ): Promise { // Special case handling for the root suite, since it can be run // with runFullTestSuite() if (node == null) { //this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); - testRun.enqueued - let testOutput = await this.runFullTestSuite(token, debuggerConfig); + this.controller.items.forEach((testSuite) => { + this.enqueTestAndChildren(testSuite, context) + }) + let testOutput = await this.runFullTestSuite(context); testOutput = TestRunner.getJsonFromOutput(testOutput); this.log.debug('Parsing the below JSON:'); this.log.debug(`${testOutput}`); @@ -213,16 +246,16 @@ export abstract class TestRunner implements vscode.Disposable { if (tests && tests.length > 0) { tests.forEach((test: { id: string; }) => { - this.handleStatus(test, testRun); + this.handleStatus(test, context); }); } // If the suite is a file, run the tests as a file rather than as separate tests. } else if (node.label.endsWith('.rb')) { // Mark selected tests as enqueued - this.enqueTestAndChildren(node, testRun) + this.enqueTestAndChildren(node, context) - testRun.started(node) - let testOutput = await this.runTestFile(token, `${node.uri?.fsPath}`, debuggerConfig); + context.started(node) + let testOutput = await this.runTestFile(`${node.uri?.fsPath}`, context); testOutput = TestRunner.getJsonFromOutput(testOutput); this.log.debug('Parsing the below JSON:'); @@ -232,7 +265,7 @@ export abstract class TestRunner implements vscode.Disposable { if (tests && tests.length > 0) { tests.forEach((test: { id: string }) => { - this.handleStatus(test, testRun); + this.handleStatus(test, context); }); } @@ -244,11 +277,11 @@ export abstract class TestRunner implements vscode.Disposable { } else { if (node.uri !== undefined && node.range !== undefined) { - testRun.started(node) + context.started(node) // Run the test at the given line, add one since the line is 0-indexed in // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(token, `${node.uri.fsPath}:${node.range?.end.line}`, debuggerConfig); + let testOutput = await this.runSingleTest(`${node.uri.fsPath}:${node.range?.end.line}`, context); testOutput = TestRunner.getJsonFromOutput(testOutput); this.log.debug('Parsing the below JSON:'); @@ -256,7 +289,7 @@ export abstract class TestRunner implements vscode.Disposable { let testMetadata = JSON.parse(testOutput); let currentTest = testMetadata.examples[0]; - this.handleStatus(currentTest, testRun); + this.handleStatus(currentTest, context); } } } @@ -280,10 +313,62 @@ export abstract class TestRunner implements vscode.Disposable { /** * Mark a test node and all its children as being queued for execution */ - private enqueTestAndChildren(test: vscode.TestItem, testRun: vscode.TestRun) { - testRun.enqueued(test) + private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { + context.enqueued(test); if (test.children && test.children.size > 0) { - test.children.forEach(child => { this.enqueTestAndChildren(child, testRun) }) + test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) + } + } + + /** + * Runs the test framework with the given command. + * + * @param testCommand Command to use to run the test framework + * @param type Type of test run for logging (full, single file, single test) + * @param context Test run context + * @return The raw output from running the test suite. + */ + runTestFramework = async (testCommand: string, type: string, context: TestRunContext) => + new Promise(async (resolve, reject) => { + this.log.info(`Running test suite: ${type}`); + + resolve(await this.spawnCancellableChild(testCommand, context)) + }); + + /** + * Spawns a child process to run a command, that will be killed + * if the cancellation token is triggered + * + * @param testCommand The command to run + * @param context Test run context for the cancellation token + * @returns Raw output from process + */ + protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { + let cancelUnsubscriber = context.token.onCancellationRequested( + (e: any) => { + this.log.debug("Cancellation requested") + this.killChild() + }, + this + ) + try { + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace?.uri.fsPath, + shell: true, + env: this.getProcessEnv() + }; + + this.log.info(`Running command: ${testCommand}`); + + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + return await this.handleChildProcess(testProcess, context); + } + finally { + cancelUnsubscriber.dispose() } } @@ -291,33 +376,77 @@ export abstract class TestRunner implements vscode.Disposable { * Runs a single test. * * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. - * @param debuggerConfig A VS Code debugger configuration. + * @param context Test run context * @return The raw output from running the test. */ - abstract runSingleTest: (token: vscode.CancellationToken, testLocation: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; + protected async runSingleTest(testLocation: string, context: TestRunContext): Promise { + this.log.info(`Running single test: ${testLocation}`); + return await this.runTestFramework( + this.getSingleTestCommand(testLocation, context), + "single test", + context) + } /** * Runs tests in a given file. * * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param debuggerConfig A VS Code debugger configuration. + * @param context Test run context * @return The raw output from running the tests. */ - abstract runTestFile: (token: vscode.CancellationToken, testFile: string, debuggerConfig?: vscode.DebugConfiguration) => Promise; + protected async runTestFile(testFile: string, context: TestRunContext): Promise { + this.log.info(`Running test file: ${testFile}`); + return await this.runTestFramework( + this.getTestFileCommand(testFile, context), + "test file", + context) + } /** * Runs the full test suite for the current workspace. * - * @param debuggerConfig A VS Code debugger configuration. + * @param context Test run context + * @return The raw output from running the test suite. + */ + protected async runFullTestSuite(context: TestRunContext): Promise { + this.log.info(`Running full test suite.`); + return await this.runTestFramework( + this.getFullTestSuiteCommand(context), + "all tests", + context) + } + + /** + * Gets the command to run a single test. + * + * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. + * @param context Test run context + * @return The raw output from running the test. + */ + protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; + + /** + * Gets the command to run tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param context Test run context + * @return The raw output from running the tests. + */ + protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; + + /** + * Gets the command to run the full test suite for the current workspace. + * + * @param context Test run context * @return The raw output from running the test suite. */ - abstract runFullTestSuite: (token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration) => Promise; + protected abstract getFullTestSuiteCommand(context: TestRunContext): string; /** * Handles test state based on the output returned by the test command. * * @param test The test that we want to handle. - * @param testRun Test run object for reporting test result + * @param context Test run context */ - abstract handleStatus(test: any, testRun: vscode.TestRun): void; + protected abstract handleStatus(test: any, context: TestRunContext): void; } From c11b7cc82d4ba28b1270bfe8446083979f6c0e76 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 05:35:37 +0000 Subject: [PATCH 005/108] Add missing newlines to ends of files --- src/testFactory.ts | 2 +- src/testLoader.ts | 2 +- src/testRunContext.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/testFactory.ts b/src/testFactory.ts index ba8c6dd..2b3643c 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -100,4 +100,4 @@ export class TestFactory implements vscode.Disposable { } instance.dispose() } -} \ No newline at end of file +} diff --git a/src/testLoader.ts b/src/testLoader.ts index 5125ae5..b1ba10b 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -282,4 +282,4 @@ export abstract class TestLoader implements vscode.Disposable { } }) } -} \ No newline at end of file +} diff --git a/src/testRunContext.ts b/src/testRunContext.ts index c6ca62f..7779c99 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -155,4 +155,4 @@ export class TestRunContext { } return testItem } -} \ No newline at end of file +} From b91d118e523dcbb314133268d24b41541b257635 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 06:00:06 +0000 Subject: [PATCH 006/108] Launch debugger when debugging & delete adapter.ts --- src/adapter.ts | 286 ---------------------------------------------- src/testRunner.ts | 116 +++++++++++++++---- 2 files changed, 93 insertions(+), 309 deletions(-) delete mode 100644 src/adapter.ts diff --git a/src/adapter.ts b/src/adapter.ts deleted file mode 100644 index 89a705c..0000000 --- a/src/adapter.ts +++ /dev/null @@ -1,286 +0,0 @@ -import * as vscode from 'vscode'; -import { TestAdapter, TestLoadStartedEvent, TestLoadFinishedEvent, TestRunStartedEvent, TestRunFinishedEvent, TestSuiteEvent, TestEvent } from 'vscode-test-adapter-api'; -import { Log } from 'vscode-test-adapter-util'; -import * as childProcess from 'child_process'; -import { Tests } from './tests'; -import { RspecTests } from './rspecTests'; -import { MinitestTests } from './minitestTests'; -import * as path from 'path'; - -export class RubyAdapter implements TestAdapter { - private disposables: { dispose(): void }[] = []; - - private readonly testsEmitter = new vscode.EventEmitter(); - private readonly testStatesEmitter = new vscode.EventEmitter(); - private readonly autorunEmitter = new vscode.EventEmitter(); - private testsInstance: Tests | undefined; - private currentTestFramework: string | undefined; - - get tests(): vscode.Event { return this.testsEmitter.event; } - get testStates(): vscode.Event { return this.testStatesEmitter.event; } - get autorun(): vscode.Event | undefined { return this.autorunEmitter.event; } - - constructor( - public readonly workspace: vscode.WorkspaceFolder, - private readonly log: Log, - private readonly context: vscode.ExtensionContext - ) { - this.log.info('Initializing Ruby adapter'); - - this.disposables.push(this.testsEmitter); - this.disposables.push(this.testStatesEmitter); - this.disposables.push(this.autorunEmitter); - this.disposables.push(this.createWatcher()); - this.disposables.push(this.configWatcher()); - } - - async load(): Promise { - this.log.info('Loading Ruby tests...'); - this.testsEmitter.fire({ type: 'started' }); - if (this.getTestFramework() === "rspec") { - this.log.info('Loading RSpec tests...'); - this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); - const loadedTests = await this.testsInstance.loadTests(); - this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); - } else if (this.getTestFramework() === "minitest") { - this.log.info('Loading Minitest tests...'); - this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); - const loadedTests = await this.testsInstance.loadTests(); - this.testsEmitter.fire({ type: 'finished', suite: loadedTests }); - } else { - this.log.warn('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); - this.testsEmitter.fire({ type: 'finished' }); - } - } - - async run(tests: string[], debuggerConfig?: vscode.DebugConfiguration): Promise { - this.log.info(`Running Ruby tests ${JSON.stringify(tests)}`); - this.testStatesEmitter.fire({ type: 'started', tests }); - if (!this.testsInstance) { - let testFramework = this.getTestFramework(); - if (testFramework === "rspec") { - this.testsInstance = new RspecTests(this.context, this.testStatesEmitter, this.log, this.workspace); - } else if (testFramework === "minitest") { - this.testsInstance = new MinitestTests(this.context, this.testStatesEmitter, this.log, this.workspace); - } - } - if (this.testsInstance) { - await this.testsInstance.runTests(tests, debuggerConfig); - } - } - - async debug(testsToRun: string[]): Promise { - this.log.info(`Debugging test(s) ${JSON.stringify(testsToRun)} of ${this.workspace.uri.fsPath}`); - - const config = vscode.workspace.getConfiguration('rubyTestExplorer', null) - - const debuggerConfig = { - name: "Debug Ruby Tests", - type: "Ruby", - request: "attach", - remoteHost: config.get('debuggerHost') || "127.0.0.1", - remotePort: config.get('debuggerPort') || "1234", - remoteWorkspaceRoot: "${workspaceRoot}" - } - - const testRunPromise = this.run(testsToRun, debuggerConfig); - - this.log.info('Starting the debug session'); - let debugSession: any; - try { - await this.testsInstance!.debugCommandStarted() - debugSession = await this.startDebugging(debuggerConfig); - } catch (err) { - this.log.error('Failed starting the debug session - aborting', err); - this.cancel(); - return; - } - - const subscription = this.onDidTerminateDebugSession((session) => { - if (debugSession != session) return; - this.log.info('Debug session ended'); - this.cancel(); // terminate the test run - subscription.dispose(); - }); - - await testRunPromise; - } - - cancel(): void { - if (this.testsInstance) { - this.log.info('Killing currently-running tests.'); - this.testsInstance.killChild(); - } else { - this.log.info('No tests running currently, no process to kill.'); - } - } - - dispose(): void { - this.cancel(); - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - } - - /** - * Get the configured test framework. - */ - protected getTestFramework(): string { - // Short-circuit the test framework check if we've already determined the current test framework. - if (this.currentTestFramework !== undefined) { - return this.currentTestFramework; - } - - let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string); - // If the test framework is something other than auto, return the value. - if (['rspec', 'minitest', 'none'].includes(testFramework)) { - this.currentTestFramework = testFramework; - return testFramework; - // If the test framework is auto, we need to try to detect the test framework type. - } else { - let detectedTestFramework = this.detectTestFramework(); - this.currentTestFramework = detectedTestFramework; - return detectedTestFramework; - } - } - - /** - * Detect the current test framework using 'bundle list'. - */ - protected detectTestFramework(): string { - this.log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); - - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192 - }; - - try { - // Run 'bundle list' and set the output to bundlerList. - // Execute this syncronously to avoid the test explorer getting stuck loading. - let err, stdout = childProcess.execSync('bundle list', execArgs); - - if (err) { - this.log.error(`Error while listing Bundler dependencies: ${err}`); - this.log.error(`Output: ${stdout}`); - throw err; - } - - let bundlerList = stdout.toString(); - - // Search for rspec or minitest in the output of 'bundle list'. - // The search function returns the index where the string is found, or -1 otherwise. - if (bundlerList.search('rspec-core') >= 0) { - this.log.info(`Detected RSpec test framework.`); - return 'rspec'; - } else if (bundlerList.search('minitest') >= 0) { - this.log.info(`Detected Minitest test framework.`); - return 'minitest'; - } else { - this.log.info(`Unable to automatically detect a test framework.`); - return 'none'; - } - } catch (error) { - this.log.error(error); - return 'none'; - } - } - - protected async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { - const debugSessionPromise = new Promise((resolve, reject) => { - - let subscription: vscode.Disposable | undefined; - subscription = vscode.debug.onDidStartDebugSession(debugSession => { - if ((debugSession.name === debuggerConfig.name) && subscription) { - resolve(debugSession); - subscription.dispose(); - subscription = undefined; - } - }); - - setTimeout(() => { - if (subscription) { - reject(new Error('Debug session failed to start within 5 seconds')); - subscription.dispose(); - subscription = undefined; - } - }, 5000); - }); - - const started = await vscode.debug.startDebugging(this.workspace, debuggerConfig); - if (started) { - return await debugSessionPromise; - } else { - throw new Error('Debug session couldn\'t be started'); - } - } - - protected onDidTerminateDebugSession(cb: (session: vscode.DebugSession) => any): vscode.Disposable { - return vscode.debug.onDidTerminateDebugSession(cb); - } - - /** - * Get the test directory based on the configuration value if there's a configured test framework. - */ - private getTestDirectory(): string | undefined { - let testFramework = this.getTestFramework(); - let testDirectory = ''; - if (testFramework === 'rspec') { - testDirectory = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) - || path.join('.', 'spec'); - } else if (testFramework === 'minitest') { - testDirectory = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) - || path.join('.', 'test'); - } - - if (testDirectory === '') { - return undefined; - } - - return path.join(this.workspace.uri.fsPath, testDirectory); - } - - /** - * Create a file watcher that will reload the test tree when a relevant file is changed. - */ - private createWatcher(): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(document => { - // If there isn't a configured/detected test framework, short-circuit to avoid doing unnecessary work. - if (this.currentTestFramework === 'none') { - this.log.info('No test framework configured. Ignoring file change.'); - return; - } - 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(); - - // In the case that there's no configured test directory, we shouldn't try to reload the tests. - if (testDirectory !== undefined && filename.startsWith(testDirectory)) { - this.log.info('A test file has been edited, reloading tests.'); - this.load(); - } - - // Send an autorun event when a relevant file changes. - // This only causes a run if the user has autorun enabled. - this.log.info('Sending autorun event'); - this.autorunEmitter.fire(); - } - }) - } - - private configWatcher(): vscode.Disposable { - return vscode.workspace.onDidChangeConfiguration(configChange => { - this.log.info('Configuration changed'); - if (configChange.affectsConfiguration("rubyTestExplorer")) { - this.cancel(); - this.currentTestFramework = undefined; - this.load(); - this.autorunEmitter.fire(); - } - }) - } -} diff --git a/src/testRunner.ts b/src/testRunner.ts index 1d4bf71..f6c48b2 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -190,34 +190,105 @@ export abstract class TestRunner implements vscode.Disposable { this.controller, debuggerConfig ) - const queue: vscode.TestItem[] = []; - - // Loop through all included tests, or all known tests, and add them to our queue - if (request.include) { - request.include.forEach(test => queue.push(test)); - - // For every test that was queued, try to run it. Call run.passed() or run.failed() - while (queue.length > 0 && !token.isCancellationRequested) { - const test = queue.pop()!; - - // Skip tests the user asked to exclude - if (request.exclude?.includes(test)) { - continue; + try { + const queue: vscode.TestItem[] = []; + + if (debuggerConfig) { + this.log.info(`Debugging test(s) ${JSON.stringify(request.include)}`); + + if (!this.workspace) { + this.log.error("Cannot debug without a folder opened") + context.testRun.end() + return } - await this.runNode(test, context); - - test.children.forEach(test => queue.push(test)); + this.log.info('Starting the debug session'); + let debugSession: any; + try { + await this.debugCommandStarted() + debugSession = await this.startDebugging(debuggerConfig); + } catch (err) { + this.log.error('Failed starting the debug session - aborting', err); + this.killChild(); + return; + } + + const subscription = this.onDidTerminateDebugSession((session) => { + if (debugSession != session) return; + this.log.info('Debug session ended'); + this.killChild(); // terminate the test run + subscription.dispose(); + }); } - if (token.isCancellationRequested) { - this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + else { + this.log.info(`Running test(s) ${JSON.stringify(request.include)}`); } + + // Loop through all included tests, or all known tests, and add them to our queue + if (request.include) { + request.include.forEach(test => queue.push(test)); + + // For every test that was queued, try to run it. Call run.passed() or run.failed() + while (queue.length > 0 && !token.isCancellationRequested) { + const test = queue.pop()!; + + // Skip tests the user asked to exclude + if (request.exclude?.includes(test)) { + continue; + } + + await this.runNode(test, context); + + test.children.forEach(test => queue.push(test)); + } + if (token.isCancellationRequested) { + this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + } + } else { + await this.runNode(null, context); + } + } + finally { + // Make sure to end the run after all tests have been executed: + context.testRun.end(); + } + } + + private async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { + const debugSessionPromise = new Promise((resolve, reject) => { + + let subscription: vscode.Disposable | undefined; + subscription = vscode.debug.onDidStartDebugSession(debugSession => { + if ((debugSession.name === debuggerConfig.name) && subscription) { + resolve(debugSession); + subscription.dispose(); + subscription = undefined; + } + }); + + setTimeout(() => { + if (subscription) { + reject(new Error('Debug session failed to start within 5 seconds')); + subscription.dispose(); + subscription = undefined; + } + }, 5000); + }); + + if (!this.workspace) { + throw new Error("Cannot debug without a folder open") + } + + const started = await vscode.debug.startDebugging(this.workspace, debuggerConfig); + if (started) { + return await debugSessionPromise; } else { - await this.runNode(null, context); + throw new Error('Debug session couldn\'t be started'); } - - // Make sure to end the run after all tests have been executed: - context.testRun.end(); + } + + private onDidTerminateDebugSession(cb: (session: vscode.DebugSession) => any): vscode.Disposable { + return vscode.debug.onDidTerminateDebugSession(cb); } /** @@ -233,7 +304,6 @@ export abstract class TestRunner implements vscode.Disposable { // Special case handling for the root suite, since it can be run // with runFullTestSuite() if (node == null) { - //this.testStatesEmitter.fire({ type: 'test', test: node.id, state: 'running' }); this.controller.items.forEach((testSuite) => { this.enqueTestAndChildren(testSuite, context) }) From c801e0bc22f12ddb5511946d0ce358288d0c331d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 06:04:57 +0000 Subject: [PATCH 007/108] Fix lint error in tsconfig.json --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 6c32ec1..20534dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "noUnusedLocals": true, "removeComments": true, "skipLibCheck": true - }, + } } From b802ce5684c5b8642c7dad823acb51a1a84ee309 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 07:31:32 +0000 Subject: [PATCH 008/108] Fix things enough to be able to see the UI populated with tests --- src/rspec/rspecTestLoader.ts | 4 ++-- src/testLoader.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/rspec/rspecTestLoader.ts b/src/rspec/rspecTestLoader.ts index bcacf9a..8792571 100644 --- a/src/rspec/rspecTestLoader.ts +++ b/src/rspec/rspecTestLoader.ts @@ -8,7 +8,7 @@ export class RspecTestLoader extends TestLoader { } protected getFrameworkTestDirectory(): string { - return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string) - || path.join('.', 'spec'); + let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string + return configDir ?? path.join('.', 'spec'); } } \ No newline at end of file diff --git a/src/testLoader.ts b/src/testLoader.ts index b1ba10b..3e4f6e5 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -64,6 +64,8 @@ export abstract class TestLoader implements vscode.Disposable { let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let test_location_string: string = test_location_array.join(''); test.location = parseInt(test_location_string); + test.id = test.id.replace(this.getFrameworkTestDirectory(), '') + test.file_path = test.file_path.replace(this.getFrameworkTestDirectory(), '') tests.push(test); } ); @@ -100,6 +102,10 @@ export abstract class TestLoader implements vscode.Disposable { return undefined; } + if (testDirectory.startsWith("./")) { + testDirectory = testDirectory.substring(2) + } + return path.join(this.workspace.uri.fsPath, testDirectory); } @@ -121,7 +127,7 @@ export abstract class TestLoader implements vscode.Disposable { // Remove the spec/ directory from all the file path. uniqueFiles.forEach((file) => { - splitFilesArray.push(file.replace(`${this.getTestDirectory()}`, "").split('/')); + splitFilesArray.push(file.split('/')); }); // This gets the main types of tests, e.g. features, helpers, models, requests, etc. @@ -157,7 +163,7 @@ export abstract class TestLoader implements vscode.Disposable { // Get files that are direct descendants of the spec/ directory. let topDirectoryFiles = uniqueFiles.filter((filePath) => { - return filePath.replace(`${this.getTestDirectory()}`, "").split('/').length === 1; + return filePath.split('/').length === 1; }); topDirectoryFiles.forEach((currentFile) => { @@ -187,8 +193,8 @@ export abstract class TestLoader implements vscode.Disposable { }); let currentFileLabel = directory - ? currentFile.replace(`${this.getTestDirectory()}${directory}/`, '') - : currentFile.replace(`${this.getTestDirectory()}`, ''); + ? currentFile.replace(`${this.getFrameworkTestDirectory()}${directory}/`, '') + : currentFile.replace(this.getFrameworkTestDirectory(), ''); let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); From 7a636681ba587f0bed3cfb933118f9bf6510c9d6 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 18 Feb 2022 18:31:42 +0000 Subject: [PATCH 009/108] Install jsmock and clean up source a bit in preparation for testing --- .vscodeignore | 11 +- package-lock.json | 1165 +++++++++++++++-- package.json | 5 +- .../custom_formatter.rb | 0 src/main.ts | 27 +- src/minitest/minitestTestRunner.ts | 11 +- src/rspec/rspecTestRunner.ts | 5 +- src/testFactory.ts | 10 +- src/testLoader.ts | 10 +- src/testRunner.ts | 25 +- tsconfig.json | 3 +- 11 files changed, 1124 insertions(+), 148 deletions(-) rename custom_formatter.rb => ruby/custom_formatter.rb (100%) diff --git a/.vscodeignore b/.vscodeignore index 641527d..ea7c088 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,9 +1,12 @@ -src/** -test/** +src/ +test/ +out/ +node_modules **/*.map package-lock.json tsconfig.json -.vscode/** +.vscode/ +.github/ .gitignore *.vsix -out/test/** +webpack.config.js diff --git a/package-lock.json b/package-lock.json index 31a0ad4..22cf245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "vscode-ruby-test-adapter", "version": "0.9.0", "license": "MIT", "dependencies": { @@ -18,6 +19,7 @@ "@types/split2": "^3.2.1", "@types/vscode": "^1.54.0", "glob": "^7.1.6", + "jsmockito": "^1.0.5", "mocha": "^9.1.3", "rimraf": "^3.0.0", "typescript": "^4.2.4", @@ -183,6 +185,22 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -218,6 +236,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/big-integer": { "version": "1.6.50", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", @@ -249,6 +287,31 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -289,6 +352,30 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -419,14 +506,19 @@ } }, "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -440,6 +532,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -451,6 +549,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -523,6 +630,12 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -565,9 +678,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { "ms": "2.1.2" }, @@ -597,12 +710,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "node_modules/denodeify": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", "dev": true }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -687,6 +839,15 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -725,6 +886,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -786,6 +956,12 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -853,6 +1029,69 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -876,6 +1115,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "dev": true + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -955,6 +1200,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1022,6 +1273,26 @@ "node": ">= 6" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1037,6 +1308,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -1148,18 +1425,49 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jshamcrest": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/jshamcrest/-/jshamcrest-0.6.7.tgz", + "integrity": "sha1-BNEgnNitedCTrWpbqlD049AR0Js=", + "dev": true, + "engines": [ + "node >= 0.1.0" + ] + }, + "node_modules/jsmockito": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/jsmockito/-/jsmockito-1.0.5.tgz", + "integrity": "sha1-pPn5OgrgI0ZjA2KLhrtpzym/90o=", + "dev": true, + "engines": [ + "node >= 0.1.0" + ], + "dependencies": { + "jshamcrest": "0.6.7" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -1298,6 +1606,18 @@ "node": ">=4" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1328,33 +1648,39 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "node_modules/mocha": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", - "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", + "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", "dev": true, "dependencies": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.2", + "chokidar": "3.5.3", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.1.7", + "glob": "7.2.0", "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.25", + "nanoid": "3.2.0", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "workerpool": "6.1.5", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -1371,26 +1697,6 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1403,9 +1709,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -1414,6 +1720,45 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", + "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1423,6 +1768,18 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -1435,6 +1792,24 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -1568,9 +1943,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -1579,11 +1954,48 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prebuild-install": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.0.1.tgz", + "integrity": "sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -1608,6 +2020,30 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -1727,6 +2163,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -1739,12 +2181,63 @@ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, "node_modules/simple-swizzle": { @@ -1847,9 +2340,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -1928,6 +2418,48 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -1985,6 +2517,18 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typed-rest-client": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", @@ -2059,9 +2603,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/vsce": { - "version": "1.100.2", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.100.2.tgz", - "integrity": "sha512-eDeubJNc0iav6mbTESZ90E9WcSzqAAl/lunb4KbNjRrz9tf+657i1mKhnWUyvK7Y4D8kN5NBD2FXD4FFMZj7ig==", + "version": "1.103.1", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.103.1.tgz", + "integrity": "sha512-98oKQKKRp7J/vTIk1cuzom5cezZpYpRHs3WlySdsrTCrAEipB/HvaPTc4VZ3hGZHzHXS9P5p2L0IllntJeXwiQ==", "dev": true, "dependencies": { "azure-devops-node-api": "^11.0.1", @@ -2071,6 +2615,7 @@ "denodeify": "^1.2.1", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", + "keytar": "^7.7.0", "leven": "^3.1.0", "lodash": "^4.17.15", "markdown-it": "^10.0.0", @@ -2195,6 +2740,15 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", @@ -2253,9 +2807,9 @@ } }, "node_modules/workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "node_modules/wrap-ansi": { @@ -2524,6 +3078,22 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2556,6 +3126,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "big-integer": { "version": "1.6.50", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", @@ -2578,6 +3154,30 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -2615,6 +3215,16 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2708,9 +3318,9 @@ } }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -2723,6 +3333,12 @@ "readdirp": "~3.6.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2734,6 +3350,12 @@ "wrap-ansi": "^7.0.0" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, "color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -2802,6 +3424,12 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2832,9 +3460,9 @@ "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" }, @@ -2852,12 +3480,39 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "denodeify": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", "dev": true }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -2921,6 +3576,15 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -2947,6 +3611,12 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -2996,6 +3666,12 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -3049,6 +3725,59 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3066,6 +3795,12 @@ "has-symbols": "^1.0.1" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "dev": true + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -3121,6 +3856,12 @@ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3169,6 +3910,12 @@ "debug": "4" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3184,6 +3931,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -3262,6 +4015,21 @@ "argparse": "^2.0.1" } }, + "jshamcrest": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/jshamcrest/-/jshamcrest-0.6.7.tgz", + "integrity": "sha1-BNEgnNitedCTrWpbqlD049AR0Js=", + "dev": true + }, + "jsmockito": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/jsmockito/-/jsmockito-1.0.5.tgz", + "integrity": "sha1-pPn5OgrgI0ZjA2KLhrtpzym/90o=", + "dev": true, + "requires": { + "jshamcrest": "0.6.7" + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -3271,6 +4039,16 @@ "universalify": "^2.0.0" } }, + "keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "requires": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, "kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -3384,6 +4162,12 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -3408,52 +4192,42 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "mocha": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", - "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", + "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.2", + "chokidar": "3.5.3", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.1.7", + "glob": "7.2.0", "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.25", + "nanoid": "3.2.0", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "workerpool": "6.1.5", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "ms": { @@ -3468,9 +4242,41 @@ "dev": true }, "nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "dev": true + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node-abi": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", + "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "dev": true, + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true }, "normalize-path": { @@ -3479,6 +4285,18 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -3488,6 +4306,18 @@ "boolbase": "^1.0.0" } }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -3594,16 +4424,47 @@ "dev": true }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "prebuild-install": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.0.1.tgz", + "integrity": "sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -3622,6 +4483,26 @@ "safe-buffer": "^5.1.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + } + } + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -3708,6 +4589,12 @@ "randombytes": "^2.1.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -3725,6 +4612,29 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -3871,6 +4781,44 @@ "has-flag": "^4.0.0" } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -3916,6 +4864,15 @@ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "typed-rest-client": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", @@ -3980,9 +4937,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "vsce": { - "version": "1.100.2", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.100.2.tgz", - "integrity": "sha512-eDeubJNc0iav6mbTESZ90E9WcSzqAAl/lunb4KbNjRrz9tf+657i1mKhnWUyvK7Y4D8kN5NBD2FXD4FFMZj7ig==", + "version": "1.103.1", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.103.1.tgz", + "integrity": "sha512-98oKQKKRp7J/vTIk1cuzom5cezZpYpRHs3WlySdsrTCrAEipB/HvaPTc4VZ3hGZHzHXS9P5p2L0IllntJeXwiQ==", "dev": true, "requires": { "azure-devops-node-api": "^11.0.1", @@ -3992,6 +4949,7 @@ "denodeify": "^1.2.1", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", + "keytar": "^7.7.0", "leven": "^3.1.0", "lodash": "^4.17.15", "markdown-it": "^10.0.0", @@ -4088,6 +5046,15 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "winston": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", @@ -4136,9 +5103,9 @@ } }, "workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 06cd546..5d54e26 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/split2": "^3.2.1", "@types/vscode": "^1.54.0", "glob": "^7.1.6", + "jsmockito": "^1.0.5", "mocha": "^9.1.3", "rimraf": "^3.0.0", "typescript": "^4.2.4", @@ -59,9 +60,7 @@ "engines": { "vscode": "^1.54.0" }, - "extensionDependencies": [ - "hbenl.vscode-test-explorer" - ], + "extensionDependencies": [], "activationEvents": [ "onLanguage:ruby", "onLanguage:erb", diff --git a/custom_formatter.rb b/ruby/custom_formatter.rb similarity index 100% rename from custom_formatter.rb rename to ruby/custom_formatter.rb diff --git a/src/main.ts b/src/main.ts index a552ad8..3455afd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,9 +3,30 @@ import { getExtensionLogger } from "@vscode-logging/logger"; import { getTestFramework } from './frameworkDetector'; import { TestFactory } from './testFactory'; +export const guessWorkspaceFolder = async () => { + if (!vscode.workspace.workspaceFolders) { + return undefined; + } + + if (vscode.workspace.workspaceFolders.length < 2) { + return vscode.workspace.workspaceFolders[0]; + } + + for (const folder of vscode.workspace.workspaceFolders) { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + return folder; + } catch { + // ignored + } + } + + return undefined; +}; + export async function activate(context: vscode.ExtensionContext) { let config = vscode.workspace.getConfiguration('rubyTestExplorer', null) - + const log = getExtensionLogger({ extName: "RubyTestExplorer", level: "info", // See LogLevel type in @vscode-logging/types for possible logLevels @@ -18,9 +39,7 @@ export async function activate(context: vscode.ExtensionContext) { log.error("No workspace opened") } - const workspace: vscode.WorkspaceFolder | null = vscode.workspace.workspaceFolders - ? vscode.workspace.workspaceFolders[0] - : null; + const workspace: vscode.WorkspaceFolder | undefined = await guessWorkspaceFolder(); let testFramework: string = getTestFramework(log); const debuggerConfig: vscode.DebugConfiguration = { diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 6ab9439..2d9d546 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -72,15 +72,6 @@ export class MinitestTestRunner extends TestRunner { return directory || './test/'; } - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return this.context.asAbsolutePath('./ruby'); - } - /** * Get the env vars to run the subprocess with. * @@ -89,7 +80,7 @@ export class MinitestTestRunner extends TestRunner { protected getProcessEnv(): any { return Object.assign({}, process.env, { "RAILS_ENV": "test", - "EXT_DIR": this.getRubyScriptsLocation(), + "EXT_DIR": this.rubyScriptPath, "TESTS_DIR": this.getTestDirectory(), "TESTS_PATTERN": this.getFilePattern().join(',') }); diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 2a55046..40b51f5 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; +import * as path from 'path'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; @@ -111,7 +112,7 @@ export class RspecTestRunner extends TestRunner { * @return The spec directory */ protected getCustomFormatterLocation(): string { - return this.context.asAbsolutePath('./custom_formatter.rb'); + return path.join(this.rubyScriptPath, '/custom_formatter.rb'); } /** @@ -136,7 +137,7 @@ export class RspecTestRunner extends TestRunner { */ protected getProcessEnv(): any { return Object.assign({}, process.env, { - "EXT_DIR": this.getRubyScriptsLocation(), + "EXT_DIR": this.rubyScriptPath, }); } diff --git a/src/testFactory.ts b/src/testFactory.ts index 2b3643c..c33b428 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -11,15 +11,17 @@ export class TestFactory implements vscode.Disposable { private runner: RspecTestRunner | MinitestTestRunner | null = null; protected disposables: { dispose(): void }[] = []; protected framework: string; + private readonly rubyScriptPath: string; constructor( protected readonly log: IVSCodeExtLogger, protected readonly context: vscode.ExtensionContext, - protected readonly workspace: vscode.WorkspaceFolder | null, + protected readonly workspace: vscode.WorkspaceFolder | undefined, protected readonly controller: vscode.TestController ) { this.disposables.push(this.configWatcher()); this.framework = getTestFramework(this.log); + this.rubyScriptPath = vscode.Uri.joinPath(this.context.extensionUri, 'ruby').fsPath; } dispose(): void { @@ -33,13 +35,13 @@ export class TestFactory implements vscode.Disposable { if (!this.runner) { this.runner = this.framework == "rspec" ? new RspecTestRunner( - this.context, + this.rubyScriptPath, this.log, this.workspace, this.controller ) : new MinitestTestRunner( - this.context, + this.rubyScriptPath, this.log, this.workspace, this.controller @@ -54,14 +56,12 @@ export class TestFactory implements vscode.Disposable { this.loader = this.framework == "rspec" ? new RspecTestLoader( this.log, - this.context, this.workspace, this.controller, this.getRunner() ) : new MinitestTestLoader( this.log, - this.context, this.workspace, this.controller, this.getRunner()); diff --git a/src/testLoader.ts b/src/testLoader.ts index 3e4f6e5..ae62f74 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -10,8 +10,7 @@ export abstract class TestLoader implements vscode.Disposable { constructor( protected readonly log: IVSCodeExtLogger, - protected readonly context: vscode.ExtensionContext, - protected readonly workspace: vscode.WorkspaceFolder | null, + protected readonly workspace: vscode.WorkspaceFolder | undefined, protected readonly controller: vscode.TestController, protected readonly testRunner: RspecTestRunner | MinitestTestRunner ) { @@ -200,7 +199,12 @@ export abstract class TestLoader implements vscode.Disposable { // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", // but with the latter's first character stripped. - let currentFileAsAbsolutePath = `${this.workspace?.uri.fsPath}${currentFile.substring(1)}`; + let testDir = this.getTestDirectory() + if (!testDir) { + this.log.fatal("No test folder configured or workspace folder open") + throw new Error("Missing test folders") + } + let currentFileAsAbsolutePath = path.join(testDir, currentFile); let currentFileTestSuite: vscode.TestItem = this.controller.createTestItem( currentFile, diff --git a/src/testRunner.ts b/src/testRunner.ts index f6c48b2..6b3625b 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; -import * as split2 from 'split2'; +import split2 from 'split2'; import { IVSCodeExtLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; @@ -17,10 +17,10 @@ export abstract class TestRunner implements vscode.Disposable { * @param log The Test Adapter logger, for logging. */ constructor( - protected context: vscode.ExtensionContext, + protected rubyScriptPath: string, protected log: IVSCodeExtLogger, - protected workspace: vscode.WorkspaceFolder | null, - protected controller: vscode.TestController + protected workspace: vscode.WorkspaceFolder | undefined, + protected controller: vscode.TestController, ) {} /** @@ -195,7 +195,7 @@ export abstract class TestRunner implements vscode.Disposable { if (debuggerConfig) { this.log.info(`Debugging test(s) ${JSON.stringify(request.include)}`); - + if (!this.workspace) { this.log.error("Cannot debug without a folder opened") context.testRun.end() @@ -212,7 +212,7 @@ export abstract class TestRunner implements vscode.Disposable { this.killChild(); return; } - + const subscription = this.onDidTerminateDebugSession((session) => { if (debugSession != session) return; this.log.info('Debug session ended'); @@ -223,11 +223,11 @@ export abstract class TestRunner implements vscode.Disposable { else { this.log.info(`Running test(s) ${JSON.stringify(request.include)}`); } - + // Loop through all included tests, or all known tests, and add them to our queue if (request.include) { request.include.forEach(test => queue.push(test)); - + // For every test that was queued, try to run it. Call run.passed() or run.failed() while (queue.length > 0 && !token.isCancellationRequested) { const test = queue.pop()!; @@ -371,15 +371,6 @@ export abstract class TestRunner implements vscode.Disposable { }) } - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getRubyScriptsLocation(): string { - return vscode.Uri.joinPath(this.context.extensionUri, 'ruby').fsPath; - } - /** * Mark a test node and all its children as being queued for execution */ diff --git a/tsconfig.json b/tsconfig.json index 20534dc..ea993a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "noImplicitReturns": true, "noUnusedLocals": true, "removeComments": true, - "skipLibCheck": true + "skipLibCheck": true, + "esModuleInterop": true } } From d9c573d5fbe722f1b7d5e52ef433bdf4f713dea1 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 19 Feb 2022 19:04:36 +0000 Subject: [PATCH 010/108] Clean up tests, add unit test suite, and ensure all suites run --- .vscodeignore | 11 +- package-lock.json | 1141 ++++++++++------- package.json | 35 +- test/runFrameworkTests.ts | 70 + test/runMinitestTests.ts | 32 - test/runRspecTests.ts | 32 - test/stubs/noopLogger.ts | 18 + test/suite/DummyController.ts | 60 - test/suite/frameworks/minitest/index.ts | 34 - .../frameworks/minitest/minitest.test.ts | 224 ---- test/suite/frameworks/rspec/index.ts | 34 - test/suite/frameworks/rspec/rspec.test.ts | 193 --- test/suite/index.ts | 56 + test/suite/minitest/minitest.test.ts | 206 +++ test/suite/rspec/rspec.test.ts | 170 +++ .../suite/unitTests/frameworkDetector.test.ts | 40 + tsconfig.json | 3 +- 17 files changed, 1278 insertions(+), 1081 deletions(-) create mode 100644 test/runFrameworkTests.ts delete mode 100644 test/runMinitestTests.ts delete mode 100644 test/runRspecTests.ts create mode 100644 test/stubs/noopLogger.ts delete mode 100644 test/suite/DummyController.ts delete mode 100644 test/suite/frameworks/minitest/index.ts delete mode 100644 test/suite/frameworks/minitest/minitest.test.ts delete mode 100644 test/suite/frameworks/rspec/index.ts delete mode 100644 test/suite/frameworks/rspec/rspec.test.ts create mode 100644 test/suite/index.ts create mode 100644 test/suite/minitest/minitest.test.ts create mode 100644 test/suite/rspec/rspec.test.ts create mode 100644 test/suite/unitTests/frameworkDetector.test.ts diff --git a/.vscodeignore b/.vscodeignore index ea7c088..641527d 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,12 +1,9 @@ -src/ -test/ -out/ -node_modules +src/** +test/** **/*.map package-lock.json tsconfig.json -.vscode/ -.github/ +.vscode/** .gitignore *.vsix -webpack.config.js +out/test/** diff --git a/package-lock.json b/package-lock.json index 22cf245..0364673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,23 +11,26 @@ "dependencies": { "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.2.0" + "ts-mutex": "^1.0.0", + "tslib": "^2.3.1" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/mocha": "^9.0.0", + "@types/chai": "^4.3.0", + "@types/glob": "^7.2.0", + "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", - "@types/vscode": "^1.54.0", - "glob": "^7.1.6", - "jsmockito": "^1.0.5", - "mocha": "^9.1.3", - "rimraf": "^3.0.0", - "typescript": "^4.2.4", - "vsce": "^1.87.1", - "vscode-test": "^1.5.2" + "@types/vscode": "^1.64.0", + "@vscode/test-electron": "^2.1.2", + "chai": "^4.3.6", + "glob": "^7.2.0", + "mocha": "^9.2.0", + "rimraf": "^3.0.2", + "ts-mockito": "^2.6.1", + "typescript": "^4.5.5", + "vsce": "^2.6.7" }, "engines": { - "vscode": "^1.54.0" + "vscode": "^1.64.0" } }, "node_modules/@colors/colors": { @@ -57,6 +60,12 @@ "node": ">= 6" } }, + "node_modules/@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -74,15 +83,15 @@ "dev": true }, "node_modules/@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", + "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", "dev": true }, "node_modules/@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==", "dev": true }, "node_modules/@types/split2": { @@ -95,9 +104,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.61.0.tgz", - "integrity": "sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.64.0.tgz", + "integrity": "sha512-bSlAWz5WtcSL3cO9tAT/KpEH9rv5OBnm93OIIFwdCshaAiqr2bp1AUyEwW9MWeCvZBHEXc3V0fTYVdVyzDNwHA==", "dev": true }, "node_modules/@ungap/promise-all-settled": { @@ -127,6 +136,21 @@ "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" }, + "node_modules/@vscode/test-electron": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.2.tgz", + "integrity": "sha512-INjJ0YA9RgR1B/xBl8P4sxww4Dy2996f4Xn5oGTFfC0c2Mm45y/1Id8xmfuoba6tR5i8zZaUIHfEYWe7Rt4uZA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=8.9.3" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -149,12 +173,12 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/ansi-styles": { @@ -207,6 +231,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -221,9 +254,9 @@ } }, "node_modules/azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.1.0.tgz", + "integrity": "sha512-6/2YZuf+lJzJLrjXNYEA5RXAkMCb8j/4VcHD0qJQRsgG/KsRMYo0HgDh0by1FGHyZkQWY5LmQyJqCwRVUB3Y7Q==", "dev": true, "dependencies": { "tunnel": "0.0.6", @@ -257,9 +290,9 @@ ] }, "node_modules/big-integer": { - "version": "1.6.50", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", - "integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ==", + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true, "engines": { "node": ">=0.6" @@ -417,9 +450,9 @@ } }, "node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { "node": ">=10" @@ -428,6 +461,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -468,6 +519,15 @@ "node": ">=8" } }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -549,6 +609,50 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -642,16 +746,16 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", + "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", "dev": true, "dependencies": { "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" + "css-what": "^5.1.0", + "domhandler": "^4.3.0", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" @@ -693,11 +797,6 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -725,6 +824,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -740,12 +851,6 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, - "node_modules/denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", - "dev": true - }, "node_modules/detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", @@ -791,9 +896,9 @@ ] }, "node_modules/domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", "dev": true, "dependencies": { "domelementtype": "^2.2.0" @@ -1045,53 +1150,6 @@ "wide-align": "^1.1.0" } }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1101,6 +1159,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1154,9 +1221,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "node_modules/growl": { "version": "1.10.5", @@ -1216,9 +1283,9 @@ } }, "node_modules/hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1341,12 +1408,15 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/is-glob": { @@ -1425,27 +1495,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jshamcrest": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/jshamcrest/-/jshamcrest-0.6.7.tgz", - "integrity": "sha1-BNEgnNitedCTrWpbqlD049AR0Js=", - "dev": true, - "engines": [ - "node >= 0.1.0" - ] - }, - "node_modules/jsmockito": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/jsmockito/-/jsmockito-1.0.5.tgz", - "integrity": "sha1-pPn5OgrgI0ZjA2KLhrtpzym/90o=", - "dev": true, - "engines": [ - "node >= 0.1.0" - ], - "dependencies": { - "jshamcrest": "0.6.7" - } - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1483,9 +1532,9 @@ } }, "node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "dependencies": { "uc.micro": "^1.0.1" @@ -1545,6 +1594,15 @@ "triple-beam": "^1.3.0" } }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1558,14 +1616,14 @@ } }, "node_modules/markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, @@ -1573,21 +1631,15 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true - }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -1619,9 +1671,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -1697,10 +1749,28 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/ms": { + "node_modules/mocha/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -1811,9 +1881,9 @@ } }, "node_modules/object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1836,34 +1906,6 @@ "fn.name": "1.x.x" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -1936,6 +1978,15 @@ "node": ">=0.10.0" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -1997,9 +2048,9 @@ } }, "node_modules/qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -2264,12 +2315,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, "node_modules/stack-generator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", @@ -2366,29 +2411,29 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/strip-json-comments": { @@ -2503,6 +2548,23 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5" + } + }, + "node_modules/ts-mutex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", + "integrity": "sha512-D+YehFaXHIPShGernhNKQ3saE8vBmDz4tV4KReH6gtPlkkT25rV1VhEjxSypqKVMpysEBjR71I6OCXauat1W4g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -2529,6 +2591,15 @@ "node": "*" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/typed-rest-client": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", @@ -2541,9 +2612,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2560,9 +2631,9 @@ "dev": true }, "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", + "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==", "dev": true }, "node_modules/universalify": { @@ -2592,9 +2663,9 @@ } }, "node_modules/url-join": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", - "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, "node_modules/util-deprecate": { @@ -2603,31 +2674,28 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/vsce": { - "version": "1.103.1", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.103.1.tgz", - "integrity": "sha512-98oKQKKRp7J/vTIk1cuzom5cezZpYpRHs3WlySdsrTCrAEipB/HvaPTc4VZ3hGZHzHXS9P5p2L0IllntJeXwiQ==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.6.7.tgz", + "integrity": "sha512-5dEtdi/yzWQbOU7JDUSOs8lmSzzkewBR5P122BUkmXE6A/DEdFsKNsg2773NGXJTwwF1MfsOgUR6QVF3cLLJNQ==", "dev": true, "dependencies": { "azure-devops-node-api": "^11.0.1", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", "commander": "^6.1.0", - "denodeify": "^1.2.1", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "keytar": "^7.7.0", "leven": "^3.1.0", - "lodash": "^4.17.15", - "markdown-it": "^10.0.0", + "markdown-it": "^12.3.2", "mime": "^1.3.4", "minimatch": "^3.0.3", - "osenv": "^0.1.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^5.1.0", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", - "url-join": "^1.1.0", + "url-join": "^4.0.1", "xml2js": "^0.4.23", "yauzl": "^2.3.1", "yazl": "^2.2.2" @@ -2636,7 +2704,7 @@ "vsce": "vsce" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/vsce/node_modules/ansi-styles": { @@ -2710,21 +2778,6 @@ "node": ">=4" } }, - "node_modules/vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, - "dependencies": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - }, - "engines": { - "node": ">=8.9.3" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2829,6 +2882,50 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2914,6 +3011,50 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -2968,6 +3109,12 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2985,15 +3132,15 @@ "dev": true }, "@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", + "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", "dev": true }, "@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==", "dev": true }, "@types/split2": { @@ -3006,9 +3153,9 @@ } }, "@types/vscode": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.61.0.tgz", - "integrity": "sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.64.0.tgz", + "integrity": "sha512-bSlAWz5WtcSL3cO9tAT/KpEH9rv5OBnm93OIIFwdCshaAiqr2bp1AUyEwW9MWeCvZBHEXc3V0fTYVdVyzDNwHA==", "dev": true }, "@ungap/promise-all-settled": { @@ -3038,6 +3185,18 @@ "resolved": "https://registry.npmjs.org/@vscode-logging/types/-/types-0.1.4.tgz", "integrity": "sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==" }, + "@vscode/test-electron": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.2.tgz", + "integrity": "sha512-INjJ0YA9RgR1B/xBl8P4sxww4Dy2996f4Xn5oGTFfC0c2Mm45y/1Id8xmfuoba6tR5i8zZaUIHfEYWe7Rt4uZA==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + } + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3054,9 +3213,9 @@ "dev": true }, "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "ansi-styles": { @@ -3100,6 +3259,12 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -3111,9 +3276,9 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" }, "azure-devops-node-api": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.0.1.tgz", - "integrity": "sha512-YMdjAw9l5p/6leiyIloxj3k7VIvYThKjvqgiQn88r3nhT93ENwsoDS3A83CyJ4uTWzCZ5f5jCi6c27rTU5Pz+A==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.1.0.tgz", + "integrity": "sha512-6/2YZuf+lJzJLrjXNYEA5RXAkMCb8j/4VcHD0qJQRsgG/KsRMYo0HgDh0by1FGHyZkQWY5LmQyJqCwRVUB3Y7Q==", "dev": true, "requires": { "tunnel": "0.0.6", @@ -3133,9 +3298,9 @@ "dev": true }, "big-integer": { - "version": "1.6.50", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz", - "integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ==", + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, "binary": { @@ -3254,11 +3419,26 @@ } }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -3289,6 +3469,12 @@ } } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -3348,6 +3534,40 @@ "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "code-point-at": { @@ -3436,16 +3656,16 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", + "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", "dev": true, "requires": { "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" + "css-what": "^5.1.0", + "domhandler": "^4.3.0", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" } }, "css-what": { @@ -3465,13 +3685,6 @@ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } } }, "decamelize": { @@ -3489,6 +3702,15 @@ "mimic-response": "^3.1.0" } }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3501,12 +3723,6 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", - "dev": true - }, "detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", @@ -3537,9 +3753,9 @@ "dev": true }, "domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", "dev": true, "requires": { "domelementtype": "^2.2.0" @@ -3739,43 +3955,6 @@ "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } } }, "get-caller-file": { @@ -3784,6 +3963,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -3825,9 +4010,9 @@ } }, "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "growl": { "version": "1.10.5", @@ -3869,9 +4054,9 @@ "dev": true }, "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -3958,10 +4143,13 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } }, "is-glob": { "version": "4.0.3", @@ -4015,21 +4203,6 @@ "argparse": "^2.0.1" } }, - "jshamcrest": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/jshamcrest/-/jshamcrest-0.6.7.tgz", - "integrity": "sha1-BNEgnNitedCTrWpbqlD049AR0Js=", - "dev": true - }, - "jsmockito": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/jsmockito/-/jsmockito-1.0.5.tgz", - "integrity": "sha1-pPn5OgrgI0ZjA2KLhrtpzym/90o=", - "dev": true, - "requires": { - "jshamcrest": "0.6.7" - } - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -4061,9 +4234,9 @@ "dev": true }, "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "requires": { "uc.micro": "^1.0.1" @@ -4111,6 +4284,15 @@ "triple-beam": "^1.3.0" } }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4121,31 +4303,22 @@ } }, "markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "requires": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true } } @@ -4169,9 +4342,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -4228,12 +4401,29 @@ "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stream": { "version": "0.0.8", @@ -4319,9 +4509,9 @@ "dev": true }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", "dev": true }, "once": { @@ -4341,28 +4531,6 @@ "fn.name": "1.x.x" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4417,6 +4585,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4466,9 +4640,9 @@ } }, "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -4653,12 +4827,6 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, "stack-generator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", @@ -4747,23 +4915,23 @@ } }, "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^2.0.0" } }, "strip-json-comments": { @@ -4853,6 +5021,20 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, + "ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, + "ts-mutex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", + "integrity": "sha512-D+YehFaXHIPShGernhNKQ3saE8vBmDz4tV4KReH6gtPlkkT25rV1VhEjxSypqKVMpysEBjR71I6OCXauat1W4g==" + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -4873,6 +5055,12 @@ "safe-buffer": "^5.0.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typed-rest-client": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.6.tgz", @@ -4885,9 +5073,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true }, "uc.micro": { @@ -4897,9 +5085,9 @@ "dev": true }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", + "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==", "dev": true }, "universalify": { @@ -4926,9 +5114,9 @@ } }, "url-join": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", - "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, "util-deprecate": { @@ -4937,31 +5125,28 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "vsce": { - "version": "1.103.1", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.103.1.tgz", - "integrity": "sha512-98oKQKKRp7J/vTIk1cuzom5cezZpYpRHs3WlySdsrTCrAEipB/HvaPTc4VZ3hGZHzHXS9P5p2L0IllntJeXwiQ==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.6.7.tgz", + "integrity": "sha512-5dEtdi/yzWQbOU7JDUSOs8lmSzzkewBR5P122BUkmXE6A/DEdFsKNsg2773NGXJTwwF1MfsOgUR6QVF3cLLJNQ==", "dev": true, "requires": { "azure-devops-node-api": "^11.0.1", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", "commander": "^6.1.0", - "denodeify": "^1.2.1", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "keytar": "^7.7.0", "leven": "^3.1.0", - "lodash": "^4.17.15", - "markdown-it": "^10.0.0", + "markdown-it": "^12.3.2", "mime": "^1.3.4", "minimatch": "^3.0.3", - "osenv": "^0.1.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^5.1.0", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", - "url-join": "^1.1.0", + "url-join": "^4.0.1", "xml2js": "^0.4.23", "yauzl": "^2.3.1", "yazl": "^2.2.2" @@ -5025,18 +5210,6 @@ } } }, - "vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, - "requires": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5117,6 +5290,40 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "wrappy": { @@ -5166,6 +5373,40 @@ "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "yargs-parser": { diff --git a/package.json b/package.json index 5d54e26..06087e7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test explorer", "test adapter" ], + "type": "commonjs", "main": "out/src/main.js", "scripts": { "clean": "rimraf out *.vsix", @@ -36,29 +37,35 @@ "rebuild": "npm run clean && npm run build", "package": "vsce package", "publish": "vsce publish", - "test:minitest": "npm run build && node ./out/test/runMinitestTests.js", - "test:rspec": "npm run build && node ./out/test/runRspecTests.js" + "pretest": "npm run rebuild && npm run package", + "test": "node ./out/test/runFrameworkTests.js", + "test:minitest": "npm run test minitest", + "test:rspec": "npm run test rspec", + "test:unit": "npm run test unitTests" }, "dependencies": { "@vscode-logging/logger": "^1.2.3", "split2": "^4.1.0", - "tslib": "^2.2.0" + "ts-mutex": "^1.0.0", + "tslib": "^2.3.1" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/mocha": "^9.0.0", + "@types/chai": "^4.3.0", + "@types/glob": "^7.2.0", + "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", - "@types/vscode": "^1.54.0", - "glob": "^7.1.6", - "jsmockito": "^1.0.5", - "mocha": "^9.1.3", - "rimraf": "^3.0.0", - "typescript": "^4.2.4", - "vsce": "^1.87.1", - "vscode-test": "^1.5.2" + "@types/vscode": "^1.64.0", + "@vscode/test-electron": "^2.1.2", + "chai": "^4.3.6", + "glob": "^7.2.0", + "mocha": "^9.2.0", + "rimraf": "^3.0.2", + "ts-mockito": "^2.6.1", + "typescript": "^4.5.5", + "vsce": "^2.6.7" }, "engines": { - "vscode": "^1.54.0" + "vscode": "^1.64.0" }, "extensionDependencies": [], "activationEvents": [ diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts new file mode 100644 index 0000000..033c39c --- /dev/null +++ b/test/runFrameworkTests.ts @@ -0,0 +1,70 @@ +import * as path from 'path' +import { runTests, downloadAndUnzipVSCode } from '@vscode/test-electron'; + +//require('module-alias/register') + +const extensionDevelopmentPath = path.resolve(__dirname, '../'); +const allowedSuiteArguments = ["rspec", "minitest", "unitTests"] + +async function main(framework: string) { + let vscodeExecutablePath = await downloadAndUnzipVSCode('stable') + + await runTestSuite(vscodeExecutablePath, framework) +} + +/** + * Sets up and runs a test suite in a child instance of VSCode + * + * If this is run + * + * @param suite Name of the test suite to run (one of the folders in test/suite) + * @param vscodeExecutablePath Path to VS executable to use. If not provided, or doesn't exist, + * the extension test will download the latest stable VS code to run the tests with + */ +async function runTestSuite(vscodeExecutablePath: string, suite: string) { + let testsPath = path.resolve(extensionDevelopmentPath, `test/suite`) + let fixturesPath = path.resolve(extensionDevelopmentPath, `test/fixtures/${suite}`) + + console.debug(`testsPath: ${testsPath}`) + console.debug(`fixturesPath: ${fixturesPath}`) + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath: testsPath, + extensionTestsEnv: { "TEST_SUITE": suite }, + launchArgs: suite == "unitTests" ? [] : [fixturesPath], + vscodeExecutablePath: vscodeExecutablePath + }).catch((error: any) => { + console.error(error); + console.error(`Failed to run ${suite} tests`) + }) +} + +function printHelpAndExit() { + console.log("") + console.log("Please run this script with one of the following available test suite names:") + allowedSuiteArguments.forEach(name => { + console.log(` - ${name}`) + }); + console.log("") + console.log("Example:") + console.log("node ./out/test/runTestSuites.js rspec") + + process.exit(1) +} + +// Check a test suite argument was actually given +if (process.argv.length < 3) { + console.error("No test suite requested!") + printHelpAndExit() +} + +let suite = process.argv[2] +// Check the test suite argument is one that we accept +if (!allowedSuiteArguments.includes(suite)) { + console.error(`Invalid test suite requested: ${suite}`) + printHelpAndExit() +} + +console.info(`Running ${suite} tests...`) +main(process.argv[2]) diff --git a/test/runMinitestTests.ts b/test/runMinitestTests.ts deleted file mode 100644 index c84edcd..0000000 --- a/test/runMinitestTests.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as path from 'path'; -import * as cp from 'child_process'; - -import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; - -async function main() { - try { - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); - - const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') - - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) - cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { - encoding: 'utf-8', - stdio: 'inherit' - }) - - await runTests( - { - extensionDevelopmentPath, - extensionTestsPath: path.resolve(__dirname, './suite/frameworks/minitest/index'), - launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/minitest')] - } - ); - } catch (err) { - console.error(err); - console.error('Failed to run tests'); - process.exit(1); - } -} - -main(); diff --git a/test/runRspecTests.ts b/test/runRspecTests.ts deleted file mode 100644 index b121956..0000000 --- a/test/runRspecTests.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as path from 'path'; -import * as cp from 'child_process'; - -import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; - -async function main() { - try { - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); - - const vscodeExecutablePath = await downloadAndUnzipVSCode('stable') - - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath) - cp.spawnSync(cliPath, ['--install-extension', 'hbenl.vscode-test-explorer'], { - encoding: 'utf-8', - stdio: 'inherit' - }) - - await runTests( - { - extensionDevelopmentPath, - extensionTestsPath: path.resolve(__dirname, './suite/frameworks/rspec/index'), - launchArgs: [path.resolve(extensionDevelopmentPath, 'test/fixtures/rspec')] - } - ); - } catch (err) { - console.error(err); - console.error('Failed to run tests'); - process.exit(1); - } -} - -main(); diff --git a/test/stubs/noopLogger.ts b/test/stubs/noopLogger.ts new file mode 100644 index 0000000..56a8632 --- /dev/null +++ b/test/stubs/noopLogger.ts @@ -0,0 +1,18 @@ +import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; + +export function noop() {} + +export const NOOP_LOGGER: IVSCodeExtLogger = { + changeLevel: noop, + changeSourceLocationTracking: noop, + debug: noop, + error: noop, + fatal: noop, + getChildLogger(opts: { label: string }): IChildLogger { + return this; + }, + info: noop, + trace: noop, + warn: noop +}; +Object.freeze(NOOP_LOGGER); diff --git a/test/suite/DummyController.ts b/test/suite/DummyController.ts deleted file mode 100644 index 34c87cc..0000000 --- a/test/suite/DummyController.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TestAdapter, TestController, TestEvent, TestSuiteInfo } from "vscode-test-adapter-api"; - -export const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec)); - -export class DummyController implements TestController { - adapter: TestAdapter | undefined - suite: TestSuiteInfo | undefined - testEvents: { [testRunId: string]: TestEvent[] } - - constructor() { - this.testEvents = {} - } - - async load() { - await this.adapter?.load() - } - - async runTest(...testRunIds: string[]) { - await this.adapter?.run(testRunIds) - } - - registerTestAdapter(adapter: TestAdapter) { - if (this.adapter === undefined) { - this.adapter = adapter - - adapter.tests(event => { - switch (event.type) { - case 'started': - this.suite = undefined - this.testEvents = {} - break - case 'finished': - this.suite = event.suite - break - } - }) - - adapter.testStates(event => { - switch (event.type) { - case 'test': - const id = event.test - if (typeof id === "string") { - const value = this.testEvents[id] - if (!Array.isArray(value)) { - this.testEvents[id] = [event] - } else { - value.push(event) - } - } - } - }) - } - } - - unregisterTestAdapter(adapter: TestAdapter) { - if (this.adapter === adapter) { - this.adapter = undefined - } - } -} diff --git a/test/suite/frameworks/minitest/index.ts b/test/suite/frameworks/minitest/index.ts deleted file mode 100644 index 7768839..0000000 --- a/test/suite/frameworks/minitest/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; - -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd' - }); - - return new Promise((c, e) => { - glob('**.test.js', { cwd: __dirname }, (err, files) => { - if (err) { - return e(err); - } - - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - e(err); - } - }); - }); -} diff --git a/test/suite/frameworks/minitest/minitest.test.ts b/test/suite/frameworks/minitest/minitest.test.ts deleted file mode 100644 index 47855d4..0000000 --- a/test/suite/frameworks/minitest/minitest.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; -import { DummyController } from '../../DummyController'; - -suite('Extension Test for Minitest', () => { - test('Load all tests', async () => { - const controller = new DummyController() - const dirPath = vscode.workspace.workspaceFolders![0].uri.path - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - - assert.deepStrictEqual( - controller.suite, - { - type: 'suite', - id: 'root', - label: 'minitest Minitest', - children: [ - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb", - label: "abs_test.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[4]", - label: "abs positive", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[8]", - label: "abs 0", - line: 7, - type: "test" - }, - { - file: path.resolve(dirPath, "test/abs_test.rb"), - id: "./test/abs_test.rb[12]", - label: "abs negative", - line: 11, - type: "test" - } - ] - }, - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb", - label: "square_test.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb[4]", - label: "square 2", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "test/square_test.rb"), - id: "./test/square_test.rb[8]", - label: "square 3", - line: 7, - type: "test" - } - ] - } - ] - } as TestSuiteInfo - ) - }) - - test('run test success', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/square_test.rb[4]') - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[4]'], - [ - { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - { state: "passed", test: "./test/square_test.rb[4]", type: "test" }, - { state: "passed", test: "./test/square_test.rb[4]", type: "test" } - ] - ) - }) - - test('run test failure', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/square_test.rb[8]') - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][0], - { state: "running", test: "./test/square_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][1], - { state: "running", test: "./test/square_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/square_test.rb[8]'][2], - { state: "failed", test: "./test/square_test.rb[8]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/square_test.rb[8]'][3] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("Expected: 9\n Actual: 6\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.strictEqual(decoration.message, "Expected: 9\n Actual: 6") - }) - - test('run test error', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/abs_test.rb[8]') - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][0], - { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][1], - { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[8]'][2], - { state: "failed", test: "./test/abs_test.rb[8]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/abs_test.rb[8]'][3] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.ok(decoration.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) - }) - - test('run test skip', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./test/abs_test.rb[12]') - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][0], - { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][1], - { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./test/abs_test.rb[12]'][2], - { state: "skipped", test: "./test/abs_test.rb[12]", type: "test" } - ) - - const lastEvent = controller.testEvents['./test/abs_test.rb[12]'][3] - assert.strictEqual(lastEvent.state, "skipped") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.strictEqual(lastEvent.message, "Not implemented yet") - - assert.strictEqual(lastEvent.decorations, undefined) - }) -}); diff --git a/test/suite/frameworks/rspec/index.ts b/test/suite/frameworks/rspec/index.ts deleted file mode 100644 index 7768839..0000000 --- a/test/suite/frameworks/rspec/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; - -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd' - }); - - return new Promise((c, e) => { - glob('**.test.js', { cwd: __dirname }, (err, files) => { - if (err) { - return e(err); - } - - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(__dirname, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - e(err); - } - }); - }); -} diff --git a/test/suite/frameworks/rspec/rspec.test.ts b/test/suite/frameworks/rspec/rspec.test.ts deleted file mode 100644 index 06344e3..0000000 --- a/test/suite/frameworks/rspec/rspec.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -import { testExplorerExtensionId, TestHub, TestSuiteInfo } from 'vscode-test-adapter-api'; -import { DummyController } from '../../DummyController'; - -suite('Extension Test for RSpec', () => { - test('Load all tests', async () => { - const controller = new DummyController() - const dirPath = vscode.workspace.workspaceFolders![0].uri.path - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - - assert.deepStrictEqual( - controller.suite, - { - type: 'suite', - id: 'root', - label: 'rspec RSpec', - children: [ - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb", - label: "abs_spec.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/abs_spec.rb"), - id: "./spec/abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - type: "test" - } - ] - }, - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb", - label: "square_spec.rb", - type: "suite", - children: [ - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb[1:1]", - label: "finds the square of 2", - line: 3, - type: "test" - }, - { - file: path.resolve(dirPath, "spec/square_spec.rb"), - id: "./spec/square_spec.rb[1:2]", - label: "finds the square of 3", - line: 7, - type: "test" - } - ] - } - ] - } as TestSuiteInfo - ) - }) - - test('run test success', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/square_spec.rb') - - assert.deepStrictEqual( - controller.testEvents['./spec/square_spec.rb[1:1]'], - [ - { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" }, - { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" } - ] - ) - }) - - test('run test failure', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/square_spec.rb') - - assert.deepStrictEqual( - controller.testEvents['./spec/square_spec.rb[1:2]'][0], - { state: "failed", test: "./spec/square_spec.rb[1:2]", type: "test" } - ) - - const lastEvent = controller.testEvents['./spec/square_spec.rb[1:2]'][1] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.strictEqual(decoration.message, " expected: 9\n got: 6\n\n(compared using ==)\n") - }) - - test('run test error', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/abs_spec.rb[1:2]') - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:2]'][0], - { state: "running", test: "./spec/abs_spec.rb[1:2]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:2]'][1], - { state: "failed", test: "./spec/abs_spec.rb[1:2]", type: "test" } - ) - - const lastEvent = controller.testEvents['./spec/abs_spec.rb[1:2]'][2] - assert.strictEqual(lastEvent.state, "failed") - assert.strictEqual(lastEvent.line, undefined) - assert.strictEqual(lastEvent.tooltip, undefined) - assert.strictEqual(lastEvent.description, undefined) - assert.ok(lastEvent.message?.startsWith("RuntimeError:\nAbs for zero is not supported")) - - assert.strictEqual(lastEvent.decorations!.length, 1) - const decoration = lastEvent.decorations![0] - assert.strictEqual(decoration.line, 8) - assert.strictEqual(decoration.file, undefined) - assert.strictEqual(decoration.hover, undefined) - assert.ok(decoration.message?.startsWith("Abs for zero is not supported")) - }) - - test('run test skip', async () => { - const controller = new DummyController() - - const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - const testHub = testExplorerExtension.exports; - - testHub.registerTestController(controller); - - await controller.load() - await controller.runTest('./spec/abs_spec.rb[1:3]') - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:3]'][0], - { state: "running", test: "./spec/abs_spec.rb[1:3]", type: "test" } - ) - - assert.deepStrictEqual( - controller.testEvents['./spec/abs_spec.rb[1:3]'][1], - { state: "skipped", test: "./spec/abs_spec.rb[1:3]", type: "test" } - ) - }) -}); diff --git a/test/suite/index.ts b/test/suite/index.ts new file mode 100644 index 0000000..1f69734 --- /dev/null +++ b/test/suite/index.ts @@ -0,0 +1,56 @@ +import path from 'path'; +import Mocha from 'mocha'; +import glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true, + diff: true + }); + + const suite = process.env['TEST_SUITE'] ?? '' + + return new Promise((success, error) => { + let testGlob = path.join(suite, '**.test.js') + glob(testGlob, { cwd: __dirname }, (err, files) => { + if (err) { + return error(err); + } + + // Add files to the test suite + files.forEach(f => { + let fPath = path.resolve(__dirname, f) + console.log(fPath) + mocha.addFile(fPath) + }); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + printFailureCount(failures) + + // Failed tests doesn't mean we failed to _run_ the tests :) + success(); + } + }); + } catch (err) { + error(err); + } + }); + }); +} + +function printFailureCount(failures: number) { + let failureString = `* ${failures} tests failed. *` + let line = new String('*'.repeat(failureString.length)) + let space = `*${new String(' '.repeat(failureString.length - 2))}*` + console.log(line) + console.log(space) + console.log(failureString); + console.log(space) + console.log(line) + console.log("") +} diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts new file mode 100644 index 0000000..3afc62a --- /dev/null +++ b/test/suite/minitest/minitest.test.ts @@ -0,0 +1,206 @@ +import * as assert from 'assert'; +//import * as path from 'path'; +import 'mocha' + +//import * as vscode from 'vscode'; + +suite('Extension Test for Minitest', () => { + test('Load all tests', async () => { + assert.fail("Not yet fixed for new API") + // const dirPath = vscode.workspace.workspaceFolders![0].uri.path + + // await controller.load() + + // assert.deepStrictEqual( + // controller.suite, + // { + // type: 'suite', + // id: 'root', + // label: 'minitest Minitest', + // children: [ + // { + // file: path.resolve(dirPath, "test/abs_test.rb"), + // id: "./test/abs_test.rb", + // label: "abs_test.rb", + // type: "suite", + // children: [ + // { + // file: path.resolve(dirPath, "test/abs_test.rb"), + // id: "./test/abs_test.rb[4]", + // label: "abs positive", + // line: 3, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "test/abs_test.rb"), + // id: "./test/abs_test.rb[8]", + // label: "abs 0", + // line: 7, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "test/abs_test.rb"), + // id: "./test/abs_test.rb[12]", + // label: "abs negative", + // line: 11, + // type: "test" + // } + // ] + // }, + // { + // file: path.resolve(dirPath, "test/square_test.rb"), + // id: "./test/square_test.rb", + // label: "square_test.rb", + // type: "suite", + // children: [ + // { + // file: path.resolve(dirPath, "test/square_test.rb"), + // id: "./test/square_test.rb[4]", + // label: "square 2", + // line: 3, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "test/square_test.rb"), + // id: "./test/square_test.rb[8]", + // label: "square 3", + // line: 7, + // type: "test" + // } + // ] + // } + // ] + // } as TestSuiteInfo + // ) + }) + + test('run test success', async () => { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./test/square_test.rb[4]') + + // assert.deepStrictEqual( + // controller.testEvents['./test/square_test.rb[4]'], + // [ + // { state: "running", test: "./test/square_test.rb[4]", type: "test" }, + // { state: "running", test: "./test/square_test.rb[4]", type: "test" }, + // { state: "passed", test: "./test/square_test.rb[4]", type: "test" }, + // { state: "passed", test: "./test/square_test.rb[4]", type: "test" } + // ] + // ) + }) + + test('run test failure', async () => { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./test/square_test.rb[8]') + + // assert.deepStrictEqual( + // controller.testEvents['./test/square_test.rb[8]'][0], + // { state: "running", test: "./test/square_test.rb[8]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/square_test.rb[8]'][1], + // { state: "running", test: "./test/square_test.rb[8]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/square_test.rb[8]'][2], + // { state: "failed", test: "./test/square_test.rb[8]", type: "test" } + // ) + + // const lastEvent = controller.testEvents['./test/square_test.rb[8]'][3] + // assert.strictEqual(lastEvent.state, "failed") + // assert.strictEqual(lastEvent.line, undefined) + // assert.strictEqual(lastEvent.tooltip, undefined) + // assert.strictEqual(lastEvent.description, undefined) + // assert.ok(lastEvent.message?.startsWith("Expected: 9\n Actual: 6\n")) + + // assert.strictEqual(lastEvent.decorations!.length, 1) + // const decoration = lastEvent.decorations![0] + // assert.strictEqual(decoration.line, 8) + // assert.strictEqual(decoration.file, undefined) + // assert.strictEqual(decoration.hover, undefined) + // assert.strictEqual(decoration.message, "Expected: 9\n Actual: 6") + }) + + test('run test error', async () => { + assert.fail("Not yet fixed for new API") + // const controller = new DummyController() + + // const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; + // const testHub = testExplorerExtension.exports; + + // testHub.registerTestController(controller); + + // await controller.load() + // await controller.runTest('./test/abs_test.rb[8]') + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[8]'][0], + // { state: "running", test: "./test/abs_test.rb[8]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[8]'][1], + // { state: "running", test: "./test/abs_test.rb[8]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[8]'][2], + // { state: "failed", test: "./test/abs_test.rb[8]", type: "test" } + // ) + + // const lastEvent = controller.testEvents['./test/abs_test.rb[8]'][3] + // assert.strictEqual(lastEvent.state, "failed") + // assert.strictEqual(lastEvent.line, undefined) + // assert.strictEqual(lastEvent.tooltip, undefined) + // assert.strictEqual(lastEvent.description, undefined) + // assert.ok(lastEvent.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) + + // assert.strictEqual(lastEvent.decorations!.length, 1) + // const decoration = lastEvent.decorations![0] + // assert.strictEqual(decoration.line, 8) + // assert.strictEqual(decoration.file, undefined) + // assert.strictEqual(decoration.hover, undefined) + // assert.ok(decoration.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) + }) + + test('run test skip', async () => { + assert.fail("Not yet fixed for new API") + // const controller = new DummyController() + + // const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; + // const testHub = testExplorerExtension.exports; + + // testHub.registerTestController(controller); + + // await controller.load() + // await controller.runTest('./test/abs_test.rb[12]') + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[12]'][0], + // { state: "running", test: "./test/abs_test.rb[12]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[12]'][1], + // { state: "running", test: "./test/abs_test.rb[12]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./test/abs_test.rb[12]'][2], + // { state: "skipped", test: "./test/abs_test.rb[12]", type: "test" } + // ) + + // const lastEvent = controller.testEvents['./test/abs_test.rb[12]'][3] + // assert.strictEqual(lastEvent.state, "skipped") + // assert.strictEqual(lastEvent.line, undefined) + // assert.strictEqual(lastEvent.tooltip, undefined) + // assert.strictEqual(lastEvent.description, undefined) + // assert.strictEqual(lastEvent.message, "Not implemented yet") + + // assert.strictEqual(lastEvent.decorations, undefined) + }) +}); diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts new file mode 100644 index 0000000..eacec2b --- /dev/null +++ b/test/suite/rspec/rspec.test.ts @@ -0,0 +1,170 @@ +import * as assert from 'assert'; +import { instance, mock/*, reset, spy, when*/ } from 'ts-mockito' +//import * as path from 'path'; +import * as vscode from 'vscode'; +import { NOOP_LOGGER } from '../../stubs/noopLogger'; +import { RspecTestLoader } from '../../../src/rspec/rspecTestLoader'; +import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; + +suite('Extension Test for RSpec', function() { + let mockTestController = mock() + + let workspaceFolder: vscode.WorkspaceFolder | undefined = undefined; // stub this + let testController: vscode.TestController = instance(mockTestController); + let testRunner: RspecTestRunner; + let testLoader: RspecTestLoader; + + //let getTestRunner = () => new RspecTestRunner("", NOOP_LOGGER, workspaceFolder, testController) + let getTestLoader = () => new RspecTestLoader(NOOP_LOGGER, workspaceFolder, testController, testRunner) + + test('Load all tests', async function() { + testLoader = getTestLoader(); + //const dirPath = vscode.workspace.workspaceFolders![0].uri.path + + await testLoader.loadAllTests() + + assert.fail("Not yet fixed for new API") + // assert.notDeepEqual( + // testController.items, + // [ + // { + // file: path.resolve(dirPath, "spec/abs_spec.rb"), + // id: "./spec/abs_spec.rb", + // label: "abs_spec.rb", + // type: "suite", + // children: [ + // { + // file: path.resolve(dirPath, "spec/abs_spec.rb"), + // id: "./spec/abs_spec.rb[1:1]", + // label: "finds the absolute value of 1", + // line: 3, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "spec/abs_spec.rb"), + // id: "./spec/abs_spec.rb[1:2]", + // label: "finds the absolute value of 0", + // line: 7, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "spec/abs_spec.rb"), + // id: "./spec/abs_spec.rb[1:3]", + // label: "finds the absolute value of -1", + // line: 11, + // type: "test" + // } + // ] + // }, + // { + // file: path.resolve(dirPath, "spec/square_spec.rb"), + // id: "./spec/square_spec.rb", + // label: "square_spec.rb", + // type: "suite", + // children: [ + // { + // file: path.resolve(dirPath, "spec/square_spec.rb"), + // id: "./spec/square_spec.rb[1:1]", + // label: "finds the square of 2", + // line: 3, + // type: "test" + // }, + // { + // file: path.resolve(dirPath, "spec/square_spec.rb"), + // id: "./spec/square_spec.rb[1:2]", + // label: "finds the square of 3", + // line: 7, + // type: "test" + // } + // ] + // } + // ] + // } as TestSuiteInfo + // ) + }) + + test('run test success', async function() { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./spec/square_spec.rb') + + // assert.deepStrictEqual( + // controller.testEvents['./spec/square_spec.rb[1:1]'], + // [ + // { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" }, + // { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" } + // ] + // ) + }) + + test('run test failure', async function() { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./spec/square_spec.rb') + + // assert.deepStrictEqual( + // controller.testEvents['./spec/square_spec.rb[1:2]'][0], + // { state: "failed", test: "./spec/square_spec.rb[1:2]", type: "test" } + // ) + + // const lastEvent = controller.testEvents['./spec/square_spec.rb[1:2]'][1] + // assert.strictEqual(lastEvent.state, "failed") + // assert.strictEqual(lastEvent.line, undefined) + // assert.strictEqual(lastEvent.tooltip, undefined) + // assert.strictEqual(lastEvent.description, undefined) + // assert.ok(lastEvent.message?.startsWith("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n")) + + // assert.strictEqual(lastEvent.decorations!.length, 1) + // const decoration = lastEvent.decorations![0] + // assert.strictEqual(decoration.line, 8) + // assert.strictEqual(decoration.file, undefined) + // assert.strictEqual(decoration.hover, undefined) + // assert.strictEqual(decoration.message, " expected: 9\n got: 6\n\n(compared using ==)\n") + }) + + test('run test error', async function() { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./spec/abs_spec.rb[1:2]') + + // assert.deepStrictEqual( + // controller.testEvents['./spec/abs_spec.rb[1:2]'][0], + // { state: "running", test: "./spec/abs_spec.rb[1:2]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./spec/abs_spec.rb[1:2]'][1], + // { state: "failed", test: "./spec/abs_spec.rb[1:2]", type: "test" } + // ) + + // const lastEvent = controller.testEvents['./spec/abs_spec.rb[1:2]'][2] + // assert.strictEqual(lastEvent.state, "failed") + // assert.strictEqual(lastEvent.line, undefined) + // assert.strictEqual(lastEvent.tooltip, undefined) + // assert.strictEqual(lastEvent.description, undefined) + // assert.ok(lastEvent.message?.startsWith("RuntimeError:\nAbs for zero is not supported")) + + // assert.strictEqual(lastEvent.decorations!.length, 1) + // const decoration = lastEvent.decorations![0] + // assert.strictEqual(decoration.line, 8) + // assert.strictEqual(decoration.file, undefined) + // assert.strictEqual(decoration.hover, undefined) + // assert.ok(decoration.message?.startsWith("Abs for zero is not supported")) + }) + + test('run test skip', async function() { + assert.fail("Not yet fixed for new API") + // await controller.load() + // await controller.runTest('./spec/abs_spec.rb[1:3]') + + // assert.deepStrictEqual( + // controller.testEvents['./spec/abs_spec.rb[1:3]'][0], + // { state: "running", test: "./spec/abs_spec.rb[1:3]", type: "test" } + // ) + + // assert.deepStrictEqual( + // controller.testEvents['./spec/abs_spec.rb[1:3]'][1], + // { state: "skipped", test: "./spec/abs_spec.rb[1:3]", type: "test" } + // ) + }) +}); diff --git a/test/suite/unitTests/frameworkDetector.test.ts b/test/suite/unitTests/frameworkDetector.test.ts new file mode 100644 index 0000000..dd2eaf4 --- /dev/null +++ b/test/suite/unitTests/frameworkDetector.test.ts @@ -0,0 +1,40 @@ +import { NOOP_LOGGER } from "../../stubs/noopLogger"; +import { spy, when } from 'ts-mockito' +import * as vscode from 'vscode' +import { getTestFramework } from "../../../src/frameworkDetector"; + +var assert = require('assert') + +suite('frameworkDetector', function() { + suite('#getTestFramework()', function() { + let testFramework = '' + const spiedWorkspace = spy(vscode.workspace) + const configSection: { get(section: string): string | undefined } | undefined = { + get: (section: string) => testFramework + } + + test('should return rspec when configuration set to rspec', function() { + testFramework = 'rspec' + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn(configSection as vscode.WorkspaceConfiguration) + + assert.equal(getTestFramework(NOOP_LOGGER), testFramework); + }); + + test('should return minitest when configuration set to minitest', function() { + testFramework = 'minitest' + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn(configSection as vscode.WorkspaceConfiguration) + + assert.equal(getTestFramework(NOOP_LOGGER), testFramework); + }); + + test('should return none when configuration set to none', function() { + testFramework = 'none' + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn(configSection as vscode.WorkspaceConfiguration) + + assert.equal(getTestFramework(NOOP_LOGGER), testFramework); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index ea993a7..2635ede 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "noUnusedLocals": true, "removeComments": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "baseUrl": "./", } } From 19a50747e208de2c913f44bbe94a4f56e44006ec Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 19 Feb 2022 19:04:56 +0000 Subject: [PATCH 011/108] Remove leading _ from variables --- src/frameworkDetector.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/frameworkDetector.ts b/src/frameworkDetector.ts index 666465f..0a9a472 100644 --- a/src/frameworkDetector.ts +++ b/src/frameworkDetector.ts @@ -2,22 +2,22 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import { IVSCodeExtLogger } from '@vscode-logging/logger'; -export function getTestFramework(_log: IVSCodeExtLogger): string { - let testFramework: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') as string); +export function getTestFramework(log: IVSCodeExtLogger): string { + let testFramework: string = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') || ''; // If the test framework is something other than auto, return the value. if (['rspec', 'minitest', 'none'].includes(testFramework)) { return testFramework; // If the test framework is auto, we need to try to detect the test framework type. } else { - return detectTestFramework(_log); + return detectTestFramework(log); } } /** * Detect the current test framework using 'bundle list'. */ -function detectTestFramework(_log: IVSCodeExtLogger): string { - _log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); +function detectTestFramework(log: IVSCodeExtLogger): string { + log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); const execArgs: childProcess.ExecOptions = { cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, @@ -30,8 +30,8 @@ function detectTestFramework(_log: IVSCodeExtLogger): string { let err, stdout = childProcess.execSync('bundle list', execArgs); if (err) { - _log.error(`Error while listing Bundler dependencies: ${err}`); - _log.error(`Output: ${stdout}`); + log.error(`Error while listing Bundler dependencies: ${err}`); + log.error(`Output: ${stdout}`); throw err; } @@ -40,17 +40,17 @@ function detectTestFramework(_log: IVSCodeExtLogger): string { // Search for rspec or minitest in the output of 'bundle list'. // The search function returns the index where the string is found, or -1 otherwise. if (bundlerList.search('rspec-core') >= 0) { - _log.info(`Detected RSpec test framework.`); + log.info(`Detected RSpec test framework.`); return 'rspec'; } else if (bundlerList.search('minitest') >= 0) { - _log.info(`Detected Minitest test framework.`); + log.info(`Detected Minitest test framework.`); return 'minitest'; } else { - _log.info(`Unable to automatically detect a test framework.`); + log.info(`Unable to automatically detect a test framework.`); return 'none'; } } catch (error: any) { - _log.error(error); + log.error(error); return 'none'; } } From 6128f8c12f6c4863f6545f389d38385964f91122 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 21 Feb 2022 03:08:40 +0000 Subject: [PATCH 012/108] More refactoring, a few more unit tests, get rspec loading tests to pass --- bin/setup | 1 + src/config.ts | 103 ++++++++++++ src/frameworkDetector.ts | 56 ------- src/main.ts | 28 ++-- src/minitest/minitestConfig.ts | 38 +++++ src/minitest/minitestTestLoader.ts | 14 -- src/minitest/minitestTestRunner.ts | 26 +-- src/rspec/rspecConfig.ts | 98 +++++++++++ src/rspec/rspecTestLoader.ts | 14 -- src/rspec/rspecTestRunner.ts | 116 ++----------- src/testFactory.ts | 55 +++--- src/testLoader.ts | 75 +++++---- src/testRunContext.ts | 10 +- src/testRunner.ts | 84 ++++------ test/fixtures/unitTests/README.md | 1 + test/runFrameworkTests.ts | 8 +- test/stubs/stubTestItem.ts | 26 +++ test/stubs/stubTestItemCollection.ts | 41 +++++ test/suite/helpers.ts | 156 ++++++++++++++++++ test/suite/index.ts | 6 +- test/suite/rspec/rspec.test.ts | 139 ++++++++-------- test/suite/unitTests/config.test.ts | 68 ++++++++ .../suite/unitTests/frameworkDetector.test.ts | 40 ----- test/suite/unitTests/testLoader.test.ts | 156 ++++++++++++++++++ 24 files changed, 910 insertions(+), 449 deletions(-) create mode 100644 src/config.ts delete mode 100644 src/frameworkDetector.ts create mode 100644 src/minitest/minitestConfig.ts delete mode 100644 src/minitest/minitestTestLoader.ts create mode 100644 src/rspec/rspecConfig.ts delete mode 100644 src/rspec/rspecTestLoader.ts create mode 100644 test/fixtures/unitTests/README.md create mode 100644 test/stubs/stubTestItem.ts create mode 100644 test/stubs/stubTestItemCollection.ts create mode 100644 test/suite/helpers.ts create mode 100644 test/suite/unitTests/config.test.ts delete mode 100644 test/suite/unitTests/frameworkDetector.test.ts create mode 100644 test/suite/unitTests/testLoader.test.ts diff --git a/bin/setup b/bin/setup index fbafb70..4e60bc6 100755 --- a/bin/setup +++ b/bin/setup @@ -6,3 +6,4 @@ set -vx npm install bundle install --gemfile=ruby/Gemfile bundle install --gemfile=test/fixtures/minitest/Gemfile +bundle install --gemfile=test/fixtures/rspec/Gemfile diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..3644c3d --- /dev/null +++ b/src/config.ts @@ -0,0 +1,103 @@ +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; +import { IVSCodeExtLogger } from '@vscode-logging/logger'; + +export abstract class Config { + + public readonly rubyScriptPath: string; + + constructor(context: vscode.ExtensionContext | string) { + if (typeof context === "object") { + this.rubyScriptPath = vscode.Uri.joinPath(context?.extensionUri ?? vscode.Uri.file("./"), 'ruby').fsPath; + } else { + this.rubyScriptPath = (context as string) + } + } + + /** + * Printable name of the test framework + */ + public abstract frameworkName(): string + + /** + * Path in which to look for test files for the test framework in use + */ + public abstract getFrameworkTestDirectory(): string + + /** + * Get the user-configured test file pattern. + * + * @return The file pattern + */ + public getFilePattern(): Array { + let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); + return pattern || ['*_test.rb', 'test_*.rb']; + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The test directory + */ + public abstract getTestDirectory(): string; + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public abstract getProcessEnv(): any + + public static getTestFramework(log: IVSCodeExtLogger): string { + let testFramework: string = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') || ''; + // If the test framework is something other than auto, return the value. + if (['rspec', 'minitest', 'none'].includes(testFramework)) { + return testFramework; + // If the test framework is auto, we need to try to detect the test framework type. + } else { + return this.detectTestFramework(log); + } + } + + /** + * Detect the current test framework using 'bundle list'. + */ + private static detectTestFramework(log: IVSCodeExtLogger): string { + log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); + + const execArgs: childProcess.ExecOptions = { + cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, + maxBuffer: 8192 * 8192 + }; + + try { + // Run 'bundle list' and set the output to bundlerList. + // Execute this syncronously to avoid the test explorer getting stuck loading. + let err, stdout = childProcess.execSync('bundle list', execArgs); + + if (err) { + log.error(`Error while listing Bundler dependencies: ${err}`); + log.error(`Output: ${stdout}`); + throw err; + } + + let bundlerList = stdout.toString(); + + // Search for rspec or minitest in the output of 'bundle list'. + // The search function returns the index where the string is found, or -1 otherwise. + if (bundlerList.search('rspec-core') >= 0) { + log.info(`Detected RSpec test framework.`); + return 'rspec'; + } else if (bundlerList.search('minitest') >= 0) { + log.info(`Detected Minitest test framework.`); + return 'minitest'; + } else { + log.info(`Unable to automatically detect a test framework.`); + return 'none'; + } + } catch (error: any) { + log.error(error); + return 'none'; + } + } +} \ No newline at end of file diff --git a/src/frameworkDetector.ts b/src/frameworkDetector.ts deleted file mode 100644 index 0a9a472..0000000 --- a/src/frameworkDetector.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as vscode from 'vscode'; -import * as childProcess from 'child_process'; -import { IVSCodeExtLogger } from '@vscode-logging/logger'; - -export function getTestFramework(log: IVSCodeExtLogger): string { - let testFramework: string = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') || ''; - // If the test framework is something other than auto, return the value. - if (['rspec', 'minitest', 'none'].includes(testFramework)) { - return testFramework; - // If the test framework is auto, we need to try to detect the test framework type. - } else { - return detectTestFramework(log); - } -} - -/** - * Detect the current test framework using 'bundle list'. - */ -function detectTestFramework(log: IVSCodeExtLogger): string { - log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); - - const execArgs: childProcess.ExecOptions = { - cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, - maxBuffer: 8192 * 8192 - }; - - try { - // Run 'bundle list' and set the output to bundlerList. - // Execute this syncronously to avoid the test explorer getting stuck loading. - let err, stdout = childProcess.execSync('bundle list', execArgs); - - if (err) { - log.error(`Error while listing Bundler dependencies: ${err}`); - log.error(`Output: ${stdout}`); - throw err; - } - - let bundlerList = stdout.toString(); - - // Search for rspec or minitest in the output of 'bundle list'. - // The search function returns the index where the string is found, or -1 otherwise. - if (bundlerList.search('rspec-core') >= 0) { - log.info(`Detected RSpec test framework.`); - return 'rspec'; - } else if (bundlerList.search('minitest') >= 0) { - log.info(`Detected Minitest test framework.`); - return 'minitest'; - } else { - log.info(`Unable to automatically detect a test framework.`); - return 'none'; - } - } catch (error: any) { - log.error(error); - return 'none'; - } -} diff --git a/src/main.ts b/src/main.ts index 3455afd..e52e4c4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; import { getExtensionLogger } from "@vscode-logging/logger"; -import { getTestFramework } from './frameworkDetector'; import { TestFactory } from './testFactory'; +import { RspecConfig } from './rspec/rspecConfig'; +import { MinitestConfig } from './minitest/minitestConfig'; +import { Config } from './config'; export const guessWorkspaceFolder = async () => { if (!vscode.workspace.workspaceFolders) { @@ -25,35 +27,39 @@ export const guessWorkspaceFolder = async () => { }; export async function activate(context: vscode.ExtensionContext) { - let config = vscode.workspace.getConfiguration('rubyTestExplorer', null) + let extensionConfig = vscode.workspace.getConfiguration('rubyTestExplorer', null) const log = getExtensionLogger({ extName: "RubyTestExplorer", - level: "info", // See LogLevel type in @vscode-logging/types for possible logLevels + level: "debug", // See LogLevel type in @vscode-logging/types for possible logLevels logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` logOutputChannel: vscode.window.createOutputChannel("Ruby Test Explorer log"), // OutputChannel for the logger - sourceLocationTracking: false, - logConsole: (config.get('logPanel') as boolean) // define if messages should be logged to the consol + sourceLocationTracking: true, + logConsole: (extensionConfig.get('logPanel') as boolean) // define if messages should be logged to the consol }); if (vscode.workspace.workspaceFolders == undefined) { log.error("No workspace opened") } const workspace: vscode.WorkspaceFolder | undefined = await guessWorkspaceFolder(); - let testFramework: string = getTestFramework(log); + let testFramework: string = Config.getTestFramework(log); + + let testConfig = testFramework == "rspec" + ? new RspecConfig(context) + : new MinitestConfig(context) const debuggerConfig: vscode.DebugConfiguration = { name: "Debug Ruby Tests", type: "Ruby", request: "attach", - remoteHost: config.get('debuggerHost') || "127.0.0.1", - remotePort: config.get('debuggerPort') || "1234", + remoteHost: extensionConfig.get('debuggerHost') || "127.0.0.1", + remotePort: extensionConfig.get('debuggerPort') || "1234", remoteWorkspaceRoot: "${workspaceRoot}" } if (testFramework !== "none") { const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); - const testLoaderFactory = new TestFactory(log, context, workspace, controller); + const testLoaderFactory = new TestFactory(log, workspace, controller, testConfig); context.subscriptions.push(controller); testLoaderFactory.getLoader().loadAllTests(); @@ -70,13 +76,13 @@ export async function activate(context: vscode.ExtensionContext) { controller.createRunProfile( 'Run', vscode.TestRunProfileKind.Run, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + (request, token) => testLoaderFactory.getRunner().runHandler(request, token, testConfig), true // Default run profile ); controller.createRunProfile( 'Debug', vscode.TestRunProfileKind.Debug, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig) + (request, token) => testLoaderFactory.getRunner().runHandler(request, token, testConfig, debuggerConfig) ); } else { diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts new file mode 100644 index 0000000..d1156c3 --- /dev/null +++ b/src/minitest/minitestConfig.ts @@ -0,0 +1,38 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { Config } from "../config"; + +export class MinitestConfig extends Config { + public frameworkName(): string { + return "Minitest" + } + + public getFrameworkTestDirectory(): string { + return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) + || path.join('.', 'test'); + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The test directory + */ + public getTestDirectory(): string { + let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); + return directory || './test/'; + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public getProcessEnv(): any { + return Object.assign({}, process.env, { + "RAILS_ENV": "test", + "EXT_DIR": this.rubyScriptPath, + "TESTS_DIR": this.getTestDirectory(), + "TESTS_PATTERN": this.getFilePattern().join(',') + }); + } +} \ No newline at end of file diff --git a/src/minitest/minitestTestLoader.ts b/src/minitest/minitestTestLoader.ts deleted file mode 100644 index ae09eb2..0000000 --- a/src/minitest/minitestTestLoader.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TestLoader } from '../testLoader'; -import * as vscode from 'vscode'; -import * as path from 'path'; - -export class MinitestTestLoader extends TestLoader { - protected frameworkName(): string { - return "Minitest" - } - - protected getFrameworkTestDirectory(): string { - return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) - || path.join('.', 'test'); - } -} \ No newline at end of file diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 2d9d546..bcec035 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -18,7 +18,7 @@ export class MinitestTestRunner extends TestRunner { const execArgs: childProcess.ExecOptions = { cwd: this.workspace?.uri.fsPath, maxBuffer: 8192 * 8192, - env: this.getProcessEnv() + env: this.config.getProcessEnv() }; this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); @@ -62,30 +62,6 @@ export class MinitestTestRunner extends TestRunner { ); } - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); - return directory || './test/'; - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "RAILS_ENV": "test", - "EXT_DIR": this.rubyScriptPath, - "TESTS_DIR": this.getTestDirectory(), - "TESTS_PATTERN": this.getFilePattern().join(',') - }); - } - /** * Get test command with formatter and debugger arguments * diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts new file mode 100644 index 0000000..d7a071f --- /dev/null +++ b/src/rspec/rspecConfig.ts @@ -0,0 +1,98 @@ +import { Config } from '../config'; +import * as vscode from 'vscode' +import * as path from 'path'; + +export class RspecConfig extends Config { + public frameworkName(): string { + return "RSpec" + } + + /** + * Get the user-configured RSpec command, if there is one. + * + * @return The RSpec command + */ + public getTestCommand(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); + return command || `bundle exec rspec` + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + public getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` + ); + } + + /** + * Get the user-configured RSpec command and add file pattern detection. + * + * @return The RSpec command + */ + public getTestCommandWithFilePattern(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); + const dir = this.getTestDirectory().replace(/\/$/, ""); + let pattern = this.getFilePattern().map(p => `${dir}/**/${p}`).join(',') + command = command || `bundle exec rspec` + return `${command} --pattern '${pattern}'`; + } + + /** + * Get the user-configured test directory, if there is one. + * + * @return The spec directory + */ + getTestDirectory(): string { + let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); + return directory || './spec/'; + } + + /** + * Get the absolute path of the custom_formatter.rb file. + * + * @return The spec directory + */ + public getCustomFormatterLocation(): string { + return path.join(this.rubyScriptPath, '/custom_formatter.rb'); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The test command + */ + public testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` + let cmd = `${this.getTestCommand()} ${args}` + if (debuggerConfig) { + cmd = this.getDebugCommand(debuggerConfig, args); + } + return cmd + } + + /** + * Get the env vars to run the subprocess with. + * + * @return The env + */ + public getProcessEnv(): any { + return Object.assign({}, process.env, { + "EXT_DIR": this.rubyScriptPath, + }); + } + + public getFrameworkTestDirectory(): string { + let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string + return configDir ?? `.${path.sep}spec`; + } +} \ No newline at end of file diff --git a/src/rspec/rspecTestLoader.ts b/src/rspec/rspecTestLoader.ts deleted file mode 100644 index 8792571..0000000 --- a/src/rspec/rspecTestLoader.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TestLoader } from '../testLoader'; -import * as vscode from 'vscode'; -import * as path from 'path'; - -export class RspecTestLoader extends TestLoader { - protected frameworkName(): string { - return "RSpec" - } - - protected getFrameworkTestDirectory(): string { - let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string - return configDir ?? path.join('.', 'spec'); - } -} \ No newline at end of file diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 40b51f5..089704b 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; -import * as path from 'path'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; +import { RspecConfig } from './rspecConfig'; export class RspecTestRunner extends TestRunner { /** @@ -10,8 +10,9 @@ export class RspecTestRunner extends TestRunner { * * @return The raw output from the RSpec JSON formatter. */ - initTests = async (/*testFilePath: string | null*/) => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommandWithFilePattern()} --require ${this.getCustomFormatterLocation()}` + initTests = async () => new Promise((resolve, reject) => { + let cfg = this.config as RspecConfig + let cmd = `${cfg.getTestCommandWithFilePattern()} --require ${cfg.getCustomFormatterLocation()}` + ` --format CustomFormatter --order defined --dry-run`; // TODO: Only reload single file on file changed @@ -20,6 +21,8 @@ export class RspecTestRunner extends TestRunner { // } this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); + this.log.debug(`cwd: ${__dirname}`) + this.log.debug(`child process cwd: ${this.workspace?.uri.fsPath}`) // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { @@ -58,101 +61,6 @@ export class RspecTestRunner extends TestRunner { }); }); - /** - * Get the user-configured RSpec command, if there is one. - * - * @return The RSpec command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); - return command || `bundle exec rspec` - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration, args: string): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_rspec.rb ${args}` - ); - } - /** - * Get the user-configured RSpec command and add file pattern detection. - * - * @return The RSpec command - */ - 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(',') - command = command || `bundle exec rspec` - return `${command} --pattern '${pattern}'`; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The spec directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); - return directory || './spec/'; - } - - /** - * Get the absolute path of the custom_formatter.rb file. - * - * @return The spec directory - */ - protected getCustomFormatterLocation(): string { - return path.join(this.rubyScriptPath, '/custom_formatter.rb'); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` - let cmd = `${this.getTestCommand()} ${args}` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig, args); - } - return cmd - } - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected getProcessEnv(): any { - return Object.assign({}, process.env, { - "EXT_DIR": this.rubyScriptPath, - }); - } - - protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { - return `${this.testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testLocation}'` - }; - - protected getTestFileCommand(testFile: string, context: TestRunContext): string { - return `${this.testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testFile}'` - }; - - protected getFullTestSuiteCommand(context: TestRunContext): string { - return this.testCommandWithFormatterAndDebugger(context.debuggerConfig) - }; - /** * Handles test state based on the output returned by the custom RSpec formatter. * @@ -209,4 +117,16 @@ export class RspecTestRunner extends TestRunner { ) } }; + + protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testLocation}'` + }; + + protected getTestFileCommand(testFile: string, context: TestRunContext): string { + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testFile}'` + }; + + protected getFullTestSuiteCommand(context: TestRunContext): string { + return (this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig) + }; } diff --git a/src/testFactory.ts b/src/testFactory.ts index c33b428..a0eaa11 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -1,27 +1,26 @@ import * as vscode from 'vscode'; -import { getTestFramework } from './frameworkDetector'; import { IVSCodeExtLogger } from "@vscode-logging/logger"; -import { RspecTestLoader } from './rspec/rspecTestLoader'; -import { MinitestTestLoader } from './minitest/minitestTestLoader'; import { RspecTestRunner } from './rspec/rspecTestRunner'; import { MinitestTestRunner } from './minitest/minitestTestRunner'; +import { Config } from './config'; +import { TestLoader } from './testLoader'; +import { RspecConfig } from './rspec/rspecConfig'; +import { MinitestConfig } from './minitest/minitestConfig'; export class TestFactory implements vscode.Disposable { - private loader: RspecTestLoader | MinitestTestLoader | null = null; + private loader: TestLoader | null = null; private runner: RspecTestRunner | MinitestTestRunner | null = null; protected disposables: { dispose(): void }[] = []; protected framework: string; - private readonly rubyScriptPath: string; constructor( - protected readonly log: IVSCodeExtLogger, - protected readonly context: vscode.ExtensionContext, - protected readonly workspace: vscode.WorkspaceFolder | undefined, - protected readonly controller: vscode.TestController + private readonly log: IVSCodeExtLogger, + private readonly workspace: vscode.WorkspaceFolder | undefined, + private readonly controller: vscode.TestController, + private config: Config ) { this.disposables.push(this.configWatcher()); - this.framework = getTestFramework(this.log); - this.rubyScriptPath = vscode.Uri.joinPath(this.context.extensionUri, 'ruby').fsPath; + this.framework = Config.getTestFramework(this.log); } dispose(): void { @@ -35,36 +34,31 @@ export class TestFactory implements vscode.Disposable { if (!this.runner) { this.runner = this.framework == "rspec" ? new RspecTestRunner( - this.rubyScriptPath, this.log, this.workspace, - this.controller + this.controller, + this.config ) : new MinitestTestRunner( - this.rubyScriptPath, this.log, this.workspace, - this.controller + this.controller, + this.config ) this.disposables.push(this.runner); } return this.runner } - public getLoader(): RspecTestLoader | MinitestTestLoader { + public getLoader(): TestLoader { if (!this.loader) { - this.loader = this.framework == "rspec" - ? new RspecTestLoader( - this.log, - this.workspace, - this.controller, - this.getRunner() - ) - : new MinitestTestLoader( - this.log, - this.workspace, - this.controller, - this.getRunner()); + this.loader = new TestLoader( + this.log, + this.workspace, + this.controller, + this.getRunner(), + this.config + ) this.disposables.push(this.loader) } return this.loader @@ -74,9 +68,12 @@ export class TestFactory implements vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer")) { - let newFramework = getTestFramework(this.log); + let newFramework = Config.getTestFramework(this.log); if (newFramework !== this.framework) { // Config has changed to a different framework - recreate test loader and runner + this.config = newFramework == "rspec" + ? new RspecConfig(this.config.rubyScriptPath) + : new MinitestConfig(this.config.rubyScriptPath) if (this.loader) { this.disposeInstance(this.loader) this.loader = null diff --git a/src/testLoader.ts b/src/testLoader.ts index ae62f74..8e9aefc 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,19 +1,31 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { IVSCodeExtLogger } from '@vscode-logging/logger'; +import { IChildLogger } from '@vscode-logging/logger'; import { TestRunner } from './testRunner'; import { RspecTestRunner } from './rspec/rspecTestRunner'; import { MinitestTestRunner } from './minitest/minitestTestRunner'; - -export abstract class TestLoader implements vscode.Disposable { +import { Config } from './config'; + +export type ParsedTest = { + id: string, + full_description: string, + description: string, + file_path: string, + line_number: number, + location: number +} +export class TestLoader implements vscode.Disposable { protected disposables: { dispose(): void }[] = []; + private readonly log: IChildLogger; constructor( - protected readonly log: IVSCodeExtLogger, - protected readonly workspace: vscode.WorkspaceFolder | undefined, - protected readonly controller: vscode.TestController, - protected readonly testRunner: RspecTestRunner | MinitestTestRunner + readonly rootLog: IChildLogger, + private readonly workspace: vscode.WorkspaceFolder | undefined, + private readonly controller: vscode.TestController, + private readonly testRunner: RspecTestRunner | MinitestTestRunner, + private readonly config: Config ) { + this.log = rootLog.getChildLogger({label: "TestLoader"}); this.disposables.push(this.createWatcher()); this.disposables.push(this.configWatcher()); } @@ -25,16 +37,6 @@ export abstract class TestLoader implements vscode.Disposable { this.disposables = []; } - /** - * Printable name of the test framework - */ - protected abstract frameworkName(): string - - /** - * Path in which to look for test files for the test framework in use - */ - protected abstract getFrameworkTestDirectory(): string - /** * Takes the output from initTests() and parses the resulting * JSON into a TestSuiteInfo object. @@ -42,33 +44,33 @@ export abstract class TestLoader implements vscode.Disposable { * @return The full test suite. */ public async loadAllTests(): Promise { - this.log.info(`Loading Ruby tests (${this.frameworkName()})...`); + this.log.info(`Loading Ruby tests (${this.config.frameworkName()})...`); let output = await this.testRunner.initTests(); - this.log.debug('Passing raw output from dry-run into getJsonFromOutput.'); - this.log.debug(`${output}`); + this.log.debug('Passing raw output from dry-run into getJsonFromOutput', output); output = TestRunner.getJsonFromOutput(output); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${output}`); + this.log.debug('Parsing the returnd JSON', output); let testMetadata; try { testMetadata = JSON.parse(output); } catch (error) { - this.log.error(`JSON parsing failed: ${error}`); + this.log.error(`JSON parsing failed`, error); } let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; testMetadata.examples.forEach( - (test: { id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }) => { + (test: ParsedTest) => { let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let test_location_string: string = test_location_array.join(''); test.location = parseInt(test_location_string); - test.id = test.id.replace(this.getFrameworkTestDirectory(), '') - test.file_path = test.file_path.replace(this.getFrameworkTestDirectory(), '') + test.id = test.id.replace(this.config.getFrameworkTestDirectory(), '') + test.file_path = test.file_path.replace(this.config.getFrameworkTestDirectory(), '') tests.push(test); + this.log.debug("Parsed test", test) } ); + this.log.debug("Test output parsed. Building test suite", tests) let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); // // Sort the children of each test suite based on their location in the test tree. @@ -95,7 +97,9 @@ export abstract class TestLoader implements vscode.Disposable { * Get the test directory based on the configuration value if there's a configured test framework. */ private getTestDirectory(): string | undefined { - let testDirectory = this.getFrameworkTestDirectory(); + let testDirectory = this.config.getFrameworkTestDirectory(); + + this.log.debug("testDirectory", testDirectory) if (testDirectory === '' || !this.workspace) { return undefined; @@ -116,7 +120,7 @@ export abstract class TestLoader implements vscode.Disposable { * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. * @return The test suite root with its children. */ - private async getBaseTestSuite(tests: any[]): Promise { + private async getBaseTestSuite(tests: ParsedTest[]): Promise { let testSuite: vscode.TestItem[] = [] // Create an array of all test files and then abuse Sets to make it unique. @@ -124,6 +128,8 @@ export abstract class TestLoader implements vscode.Disposable { let splitFilesArray: Array = []; + this.log.debug("Building base test suite from files", uniqueFiles) + // Remove the spec/ directory from all the file path. uniqueFiles.forEach((file) => { splitFilesArray.push(file.split('/')); @@ -137,16 +143,20 @@ export abstract class TestLoader implements vscode.Disposable { } }); subdirectories = [...new Set(subdirectories)]; + this.log.debug("Found subdirectories:", subdirectories) // A nested loop to iterate through the direct subdirectories of spec/ and then // organize the files under those subdirectories. subdirectories.forEach((directory) => { - let dirPath = `${this.getTestDirectory()}${directory}/` + let dirPath = path.join(this.getTestDirectory() ?? '', directory) + this.log.debug("dirPath", dirPath) let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { return file.startsWith(dirPath); }); + this.log.debug(`Files in subdirectory:`, directory, uniqueFilesInDirectory) let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(dirPath)); + //directoryTestSuite.description = directory // Get the sets of tests for each file in the current directory. uniqueFilesInDirectory.forEach((currentFile: string) => { @@ -164,6 +174,7 @@ export abstract class TestLoader implements vscode.Disposable { let topDirectoryFiles = uniqueFiles.filter((filePath) => { return filePath.split('/').length === 1; }); + this.log.debug(`Files in top directory:`, topDirectoryFiles) topDirectoryFiles.forEach((currentFile) => { let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); @@ -192,8 +203,8 @@ export abstract class TestLoader implements vscode.Disposable { }); let currentFileLabel = directory - ? currentFile.replace(`${this.getFrameworkTestDirectory()}${directory}/`, '') - : currentFile.replace(this.getFrameworkTestDirectory(), ''); + ? currentFile.replace(`${this.config.getFrameworkTestDirectory()}${directory}/`, '') + : currentFile.replace(this.config.getFrameworkTestDirectory(), ''); let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); @@ -212,7 +223,9 @@ export abstract class TestLoader implements vscode.Disposable { vscode.Uri.file(currentFileAsAbsolutePath) ); + this.log.debug("Building tests for file", currentFile) currentFileTests.forEach((test) => { + this.log.debug("Building test", test.id) // RSpec provides test ids like "file_name.rb[1:2:3]". // This uses the digits at the end of the id to create // an array of numbers representing the location of the diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 7779c99..a7ba907 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -1,9 +1,10 @@ import * as vscode from 'vscode' -import { IVSCodeExtLogger } from '@vscode-logging/logger' +import { IChildLogger } from '@vscode-logging/logger' +import { Config } from './config' /** * Test run context - * + * * Contains all objects used for interacting with VS Test API while tests are running */ export class TestRunContext { @@ -19,10 +20,11 @@ export class TestRunContext { * @param debuggerConfig A VS Code debugger configuration. */ constructor( - public readonly log: IVSCodeExtLogger, + public readonly log: IChildLogger, public readonly token: vscode.CancellationToken, request: vscode.TestRunRequest, private readonly controller: vscode.TestController, + public readonly config: Config, public readonly debuggerConfig?: vscode.DebugConfiguration ) { this.testRun = controller.createTestRun(request) @@ -46,7 +48,7 @@ export class TestRunContext { /** * Indicates a test has errored. - * + * * This differs from the "failed" state in that it indicates a test that couldn't be executed at all, from a compilation error for example * * @param testId ID of the test item to update. diff --git a/src/testRunner.ts b/src/testRunner.ts index 6b3625b..a92dc61 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,34 +1,33 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import split2 from 'split2'; -import { IVSCodeExtLogger } from '@vscode-logging/logger'; +import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; +import { Config } from './config'; +import { RspecConfig } from './rspec/rspecConfig'; +import { MinitestConfig } from './minitest/minitestConfig'; export abstract class TestRunner implements vscode.Disposable { protected currentChildProcess: childProcess.ChildProcess | undefined; protected testSuite: vscode.TestItem[] | undefined; protected debugCommandStartedResolver: Function | undefined; protected disposables: { dispose(): void }[] = []; + protected readonly log: IChildLogger; /** - * @param context Extension context provided by vscode. - * @param testStatesEmitter An emitter for the test suite's state. * @param log The Test Adapter logger, for logging. + * @param workspace Open workspace folder + * @param controller Test controller that holds the test suite */ constructor( - protected rubyScriptPath: string, - protected log: IVSCodeExtLogger, + rootLog: IChildLogger, protected workspace: vscode.WorkspaceFolder | undefined, protected controller: vscode.TestController, - ) {} - - /** - * Get the env vars to run the subprocess with. - * - * @return The env - */ - protected abstract getProcessEnv(): any + protected config: RspecConfig | MinitestConfig + ) { + this.log = rootLog.getChildLogger({label: "TestRunner"}) + } /** * Initialise the test framework, parse tests (without executing) and retrieve the output @@ -53,23 +52,6 @@ export abstract class TestRunner implements vscode.Disposable { } } - /** - * Get the user-configured test file pattern. - * - * @return The file pattern - */ - getFilePattern(): Array { - let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); - return pattern || ['*_test.rb', 'test_*.rb']; - } - - /** - * Get the user-configured test directory, if there is one. - * - * @return The test directory - */ - abstract getTestDirectory(): string; - /** * Pull JSON out of the test framework output. * @@ -181,6 +163,7 @@ export abstract class TestRunner implements vscode.Disposable { public async runHandler( request: vscode.TestRunRequest, token: vscode.CancellationToken, + config: Config, debuggerConfig?: vscode.DebugConfiguration ) { const context = new TestRunContext( @@ -188,6 +171,7 @@ export abstract class TestRunner implements vscode.Disposable { token, request, this.controller, + config, debuggerConfig ) try { @@ -416,7 +400,7 @@ export abstract class TestRunner implements vscode.Disposable { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, - env: this.getProcessEnv() + env: context.config.getProcessEnv() }; this.log.info(`Running command: ${testCommand}`); @@ -484,24 +468,24 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test. */ - protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; - - /** - * Gets the command to run tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param context Test run context - * @return The raw output from running the tests. - */ - protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; - - /** - * Gets the command to run the full test suite for the current workspace. - * - * @param context Test run context - * @return The raw output from running the test suite. - */ - protected abstract getFullTestSuiteCommand(context: TestRunContext): string; + protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; + + /** + * Gets the command to run tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param context Test run context + * @return The raw output from running the tests. + */ + protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; + + /** + * Gets the command to run the full test suite for the current workspace. + * + * @param context Test run context + * @return The raw output from running the test suite. + */ + protected abstract getFullTestSuiteCommand(context: TestRunContext): string; /** * Handles test state based on the output returned by the test command. @@ -509,5 +493,5 @@ export abstract class TestRunner implements vscode.Disposable { * @param test The test that we want to handle. * @param context Test run context */ - protected abstract handleStatus(test: any, context: TestRunContext): void; + protected abstract handleStatus(test: any, context: TestRunContext): void; } diff --git a/test/fixtures/unitTests/README.md b/test/fixtures/unitTests/README.md new file mode 100644 index 0000000..632d8b2 --- /dev/null +++ b/test/fixtures/unitTests/README.md @@ -0,0 +1 @@ +# This folder is only here to be a workspace for the VSC instance running unit tests to open diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 033c39c..16a2f33 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -1,9 +1,7 @@ import * as path from 'path' import { runTests, downloadAndUnzipVSCode } from '@vscode/test-electron'; -//require('module-alias/register') - -const extensionDevelopmentPath = path.resolve(__dirname, '../'); +const extensionDevelopmentPath = path.resolve(__dirname, '../../'); const allowedSuiteArguments = ["rspec", "minitest", "unitTests"] async function main(framework: string) { @@ -22,7 +20,7 @@ async function main(framework: string) { * the extension test will download the latest stable VS code to run the tests with */ async function runTestSuite(vscodeExecutablePath: string, suite: string) { - let testsPath = path.resolve(extensionDevelopmentPath, `test/suite`) + let testsPath = path.resolve(__dirname, `suite`) let fixturesPath = path.resolve(extensionDevelopmentPath, `test/fixtures/${suite}`) console.debug(`testsPath: ${testsPath}`) @@ -32,7 +30,7 @@ async function runTestSuite(vscodeExecutablePath: string, suite: string) { extensionDevelopmentPath, extensionTestsPath: testsPath, extensionTestsEnv: { "TEST_SUITE": suite }, - launchArgs: suite == "unitTests" ? [] : [fixturesPath], + launchArgs: [fixturesPath], vscodeExecutablePath: vscodeExecutablePath }).catch((error: any) => { console.error(error); diff --git a/test/stubs/stubTestItem.ts b/test/stubs/stubTestItem.ts new file mode 100644 index 0000000..b3c61a8 --- /dev/null +++ b/test/stubs/stubTestItem.ts @@ -0,0 +1,26 @@ +import * as vscode from 'vscode' +import { StubTestItemCollection } from './stubTestItemCollection'; + +export class StubTestItem implements vscode.TestItem { + id: string; + uri: vscode.Uri | undefined; + children: vscode.TestItemCollection; + parent: vscode.TestItem | undefined; + tags: readonly vscode.TestTag[]; + canResolveChildren: boolean; + busy: boolean; + label: string; + description: string | undefined; + range: vscode.Range | undefined; + error: string | vscode.MarkdownString | undefined; + + constructor(id: string, label: string, uri?: vscode.Uri) { + this.id = id + this.label = label + this.uri = uri + this.children = new StubTestItemCollection + this.tags = [] + this.canResolveChildren = false + this.busy = false + } +} \ No newline at end of file diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts new file mode 100644 index 0000000..5d16bb3 --- /dev/null +++ b/test/stubs/stubTestItemCollection.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode' + +export class StubTestItemCollection implements vscode.TestItemCollection { + private testIds: { [name: string]: number } = {}; + private data: vscode.TestItem[] = [] + size: number = 0; + + replace(items: readonly vscode.TestItem[]): void { + this.data = [] + items.forEach(item => { + this.testIds[item.id] = this.data.length + this.data.push(item) + }) + this.size = this.data.length; + } + + forEach(callback: (item: vscode.TestItem, collection: vscode.TestItemCollection) => unknown, thisArg?: unknown): void { + this.data.forEach((element: vscode.TestItem) => { + return callback(element, this) + }); + } + + add(item: vscode.TestItem): void { + this.testIds[item.id] = this.data.length + this.data.push(item) + this.size++ + } + + delete(itemId: string): void { + let index = this.testIds[itemId] + if (index !== undefined || -1) { + this.data.splice(index) + delete this.testIds[itemId] + this.size-- + } + } + + get(itemId: string): vscode.TestItem | undefined { + return this.data[this.testIds[itemId]] + } +} \ No newline at end of file diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts new file mode 100644 index 0000000..1609c5c --- /dev/null +++ b/test/suite/helpers.ts @@ -0,0 +1,156 @@ +import * as vscode from 'vscode' +import * as path from 'path'; +import { expect } from 'chai' +import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; +import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; +import { anyString, anything, mock, when } from 'ts-mockito'; + +const dirPath = vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri + : vscode.Uri.file(path.resolve('./')) + +export function noop() {} + +const NOOP_LOGGER: IVSCodeExtLogger = { + changeLevel: noop, + changeSourceLocationTracking: noop, + debug: noop, + error: noop, + fatal: noop, + getChildLogger(opts: { label: string }): IChildLogger { + return this; + }, + info: noop, + trace: noop, + warn: noop +} +Object.freeze(NOOP_LOGGER) + +function writeStdOutLogMsg(level: string, msg: string, ...args: any[]): void { + console.log(`[${level}] ${msg}${args.length > 0 ? ':' : ''}`) + args.forEach((arg) => { + console.log(`${JSON.stringify(arg)}`) + }) + console.log('----------') +} + +function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogger { + let prependLabel = (l:string, m:string):string => `${l}: ${m}` + return { + ...parent, + debug: (msg: string, ...args: any[]) => { parent.debug(prependLabel(label, msg), ...args) }, + error: (msg: string, ...args: any[]) => { parent.error(prependLabel(label, msg), ...args) }, + fatal: (msg: string, ...args: any[]) => { parent.fatal(prependLabel(label, msg), ...args) }, + info: (msg: string, ...args: any[]) => { parent.info(prependLabel(label, msg), ...args) }, + trace: (msg: string, ...args: any[]) => { parent.trace(prependLabel(label, msg), ...args) }, + warn: (msg: string, ...args: any[]) => { parent.warn(prependLabel(label, msg), ...args) } + } +} + +const STDOUT_LOGGER: IVSCodeExtLogger = { + changeLevel: noop, + changeSourceLocationTracking: noop, + debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, + error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, + fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, + getChildLogger(opts: { label: string }): IChildLogger { + return createChildLogger(this, opts.label); + }, + info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, + trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, + warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } +} +Object.freeze(STDOUT_LOGGER) + +export function noop_logger(): IVSCodeExtLogger { return NOOP_LOGGER } +export function stdout_logger(): IVSCodeExtLogger { return STDOUT_LOGGER } + +/** + * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values + */ +export type TestItemExpectation = { + id: string, + file: string, + label: string, + line?: number, + children?: TestItemExpectation[] +} + +/** + * Assert that a {@link vscode.TestItem TestItem} matches the expected values + * @param testItem {@link vscode.TestItem TestItem} to check + * @param expectation {@link TestItemExpectation} to check against + */ +export function testItemMatches(testItem: vscode.TestItem, expectation: TestItemExpectation | undefined) { + if (!expectation) expect.fail("No expectation given") + + expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) + expect(testItem.uri).to.not.be.undefined + expect(testItem.uri).to.eql(vscode.Uri.joinPath(dirPath, expectation.file), `uri mismatch (id: ${expectation.id})`) + if (expectation.children && expectation.children.length > 0) { + expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) + let i = 0; + testItem.children.forEach((child) => { + testItemMatches(child, expectation.children![i]) + i++ + }) + } + expect(testItem.canResolveChildren).to.be.false + expect(testItem.label).to.eq(expectation.label, `label mismatch (id: ${expectation.id})`) + expect(testItem.description).to.be.undefined + //expect(testItem.description).to.eq(expectation.label, 'description mismatch') + if (expectation.line) { + expect(testItem.range).to.not.be.undefined + expect(testItem.range?.start.line).to.eq(expectation.line, `line number mismatch (id: ${expectation.id})`) + } else { + expect(testItem.range).to.be.undefined + } + expect(testItem.error).to.be.undefined +} + +/** + * Loops through an array of {@link vscode.TestItem TestItem}s and asserts whether each in turn matches the expectation with the same index + * @param testItems TestItems to check + * @param expectation Array of {@link TestItemExpectation}s to compare to + */ +export function testItemArrayMatches(testItems: readonly vscode.TestItem[], expectation: TestItemExpectation[]) { + testItems.forEach((testItem: vscode.TestItem, i: number) => { + testItemMatches(testItem, expectation[i]) + }) +} + +/** + * Loops through an array of {@link vscode.TestItem TestItem}s and asserts whether each in turn matches the expectation with the same index + * @param testItems TestItems to check + * @param expectation Array of {@link TestItemExpectation}s to compare to + */ + export function testItemCollectionMatches(testItems: vscode.TestItemCollection, expectation: TestItemExpectation[]) { + expect(testItems.size).to.eq(expectation.length) + let i = 0; + testItems.forEach((testItem: vscode.TestItem) => { + testItemMatches(testItem, expectation[i]) + i++ + }) +} + +export function setupMockTestController(): vscode.TestController { + let mockTestController = mock() + let createTestItem = (id: string, label: string, uri: vscode.Uri | undefined) => { + return { + id: id, + label: label, + uri: uri, + canResolveChildren: false, + parent: undefined, + tags: [], + busy: false, + range: undefined, + error: undefined, + children: new StubTestItemCollection(), + } + } + when(mockTestController.createTestItem(anyString(), anyString(), anything())).thenCall(createTestItem) + let testItems = new StubTestItemCollection() + when(mockTestController.items).thenReturn(testItems) + return mockTestController +} \ No newline at end of file diff --git a/test/suite/index.ts b/test/suite/index.ts index 1f69734..946d782 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -7,12 +7,16 @@ export function run(): Promise { const mocha = new Mocha({ ui: 'tdd', color: true, - diff: true + diff: true, + bail: false, + fullTrace: true }); const suite = process.env['TEST_SUITE'] ?? '' return new Promise((success, error) => { + console.log(`cwd: ${__dirname}`) + console.log(`Test suite path: ${suite}`) let testGlob = path.join(suite, '**.test.js') glob(testGlob, { cwd: __dirname }, (err, files) => { if (err) { diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index eacec2b..d79c1e4 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,86 +1,83 @@ import * as assert from 'assert'; -import { instance, mock/*, reset, spy, when*/ } from 'ts-mockito' -//import * as path from 'path'; import * as vscode from 'vscode'; -import { NOOP_LOGGER } from '../../stubs/noopLogger'; -import { RspecTestLoader } from '../../../src/rspec/rspecTestLoader'; +import * as path from 'path' +import { instance, reset } from 'ts-mockito' +import { setupMockTestController, stdout_logger, testItemCollectionMatches } from '../helpers'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; +import { TestLoader } from '../../../src/testLoader'; +import { RspecConfig } from '../../../src/rspec/rspecConfig'; suite('Extension Test for RSpec', function() { - let mockTestController = mock() + let mockTestController: vscode.TestController + let testController: vscode.TestController + let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] - let workspaceFolder: vscode.WorkspaceFolder | undefined = undefined; // stub this - let testController: vscode.TestController = instance(mockTestController); let testRunner: RspecTestRunner; - let testLoader: RspecTestLoader; + let testLoader: TestLoader; + + this.beforeEach(async function() { + mockTestController = setupMockTestController() + testController = instance(mockTestController) + let config = new RspecConfig(path.resolve("./ruby")) + testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config) + testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config); + }) - //let getTestRunner = () => new RspecTestRunner("", NOOP_LOGGER, workspaceFolder, testController) - let getTestLoader = () => new RspecTestLoader(NOOP_LOGGER, workspaceFolder, testController, testRunner) + this.afterEach(function () { + reset(mockTestController) + }) test('Load all tests', async function() { - testLoader = getTestLoader(); - //const dirPath = vscode.workspace.workspaceFolders![0].uri.path - await testLoader.loadAllTests() - assert.fail("Not yet fixed for new API") - // assert.notDeepEqual( - // testController.items, - // [ - // { - // file: path.resolve(dirPath, "spec/abs_spec.rb"), - // id: "./spec/abs_spec.rb", - // label: "abs_spec.rb", - // type: "suite", - // children: [ - // { - // file: path.resolve(dirPath, "spec/abs_spec.rb"), - // id: "./spec/abs_spec.rb[1:1]", - // label: "finds the absolute value of 1", - // line: 3, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "spec/abs_spec.rb"), - // id: "./spec/abs_spec.rb[1:2]", - // label: "finds the absolute value of 0", - // line: 7, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "spec/abs_spec.rb"), - // id: "./spec/abs_spec.rb[1:3]", - // label: "finds the absolute value of -1", - // line: 11, - // type: "test" - // } - // ] - // }, - // { - // file: path.resolve(dirPath, "spec/square_spec.rb"), - // id: "./spec/square_spec.rb", - // label: "square_spec.rb", - // type: "suite", - // children: [ - // { - // file: path.resolve(dirPath, "spec/square_spec.rb"), - // id: "./spec/square_spec.rb[1:1]", - // label: "finds the square of 2", - // line: 3, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "spec/square_spec.rb"), - // id: "./spec/square_spec.rb[1:2]", - // label: "finds the square of 3", - // line: 7, - // type: "test" - // } - // ] - // } - // ] - // } as TestSuiteInfo - // ) + const testSuite = testController.items + testItemCollectionMatches(testSuite, [ + { + file: "spec/abs_spec.rb", + id: "abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + file: "spec/abs_spec.rb", + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + }, + { + file: "spec/abs_spec.rb", + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + }, + { + file: "spec/abs_spec.rb", + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + ] + }, + { + file: "spec/square_spec.rb", + id: "square_spec.rb", + label: "square_spec.rb", + children: [ + { + file: "spec/square_spec.rb", + id: "square_spec.rb[1:1]", + label: "finds the square of 2", + line: 3, + }, + { + file: "spec/square_spec.rb", + id: "square_spec.rb[1:2]", + label: "finds the square of 3", + line: 7, + } + ] + } + ] + ) }) test('run test success', async function() { diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts new file mode 100644 index 0000000..b6ab67f --- /dev/null +++ b/test/suite/unitTests/config.test.ts @@ -0,0 +1,68 @@ +import { noop_logger } from "../helpers"; +import { spy, when } from 'ts-mockito' +import * as vscode from 'vscode' +import { Config } from "../../../src/config"; +import { RspecConfig } from "../../../src/rspec/rspecConfig"; +import { expect } from "chai"; + +suite('Config', function() { + let setConfig = (testFramework: string) => { + let spiedWorkspace = spy(vscode.workspace) + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn({ get: (section: string) => testFramework } as vscode.WorkspaceConfiguration) + } + + suite('#getTestFramework()', function() { + test('should return rspec when configuration set to rspec', function() { + let testFramework = "rspec" + setConfig(testFramework) + + expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + }); + + test('should return minitest when configuration set to minitest', function() { + let testFramework = 'minitest' + setConfig(testFramework) + + expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + }); + + test('should return none when configuration set to none', function() { + let testFramework = 'none' + setConfig(testFramework) + + expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + }); + }); + + suite("Rspec specific tests", function() { + test("#getTestCommandWithFilePattern", function() { + const spiedWorkspace = spy(vscode.workspace) + const configSection: { get(section: string): any | undefined } | undefined = { + get: (section: string) => { + switch (section) { + case "framework": + return "rspec" + case "filePattern": + return ['*_test.rb', 'test_*.rb'] + default: + return undefined + } + } + } + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn(configSection as vscode.WorkspaceConfiguration) + + let config = new RspecConfig("../../../ruby") + expect(config.getTestCommandWithFilePattern()).to + .eq("bundle exec rspec --pattern './spec/**/*_test.rb,./spec/**/test_*.rb'") + }) + + suite("#getFrameworkTestDirectory()", function() { + test("with no config set, it returns ./spec", function() { + let config = new RspecConfig("../../../ruby") + expect(config.getFrameworkTestDirectory()).to.eq("./spec") + }) + }) + }) +}); diff --git a/test/suite/unitTests/frameworkDetector.test.ts b/test/suite/unitTests/frameworkDetector.test.ts deleted file mode 100644 index dd2eaf4..0000000 --- a/test/suite/unitTests/frameworkDetector.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NOOP_LOGGER } from "../../stubs/noopLogger"; -import { spy, when } from 'ts-mockito' -import * as vscode from 'vscode' -import { getTestFramework } from "../../../src/frameworkDetector"; - -var assert = require('assert') - -suite('frameworkDetector', function() { - suite('#getTestFramework()', function() { - let testFramework = '' - const spiedWorkspace = spy(vscode.workspace) - const configSection: { get(section: string): string | undefined } | undefined = { - get: (section: string) => testFramework - } - - test('should return rspec when configuration set to rspec', function() { - testFramework = 'rspec' - when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) - .thenReturn(configSection as vscode.WorkspaceConfiguration) - - assert.equal(getTestFramework(NOOP_LOGGER), testFramework); - }); - - test('should return minitest when configuration set to minitest', function() { - testFramework = 'minitest' - when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) - .thenReturn(configSection as vscode.WorkspaceConfiguration) - - assert.equal(getTestFramework(NOOP_LOGGER), testFramework); - }); - - test('should return none when configuration set to none', function() { - testFramework = 'none' - when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) - .thenReturn(configSection as vscode.WorkspaceConfiguration) - - assert.equal(getTestFramework(NOOP_LOGGER), testFramework); - }); - }); -}); diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts new file mode 100644 index 0000000..798a66c --- /dev/null +++ b/test/suite/unitTests/testLoader.test.ts @@ -0,0 +1,156 @@ +import { setupMockTestController, stdout_logger, testItemArrayMatches } from "../helpers"; +import { instance, mock, spy, when } from 'ts-mockito' +import * as vscode from 'vscode' +import * as path from 'path' +import { expect } from "chai"; +import { ParsedTest, TestLoader } from "../../../src/testLoader"; +import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; +import { RspecConfig } from "../../../src/rspec/rspecConfig"; + +suite('TestLoader', function() { + let mockTestController = setupMockTestController() + let mockTestRunner = mock() + let loader:TestLoader + + suite('#getBaseTestSuite()', function() { + this.beforeEach(function() { + let spiedWorkspace = spy(vscode.workspace) + when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) + .thenReturn({ get: (section: string) => { + section == "framework" ? "rspec" : undefined + }} as vscode.WorkspaceConfiguration) + + loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], instance(mockTestController), instance(mockTestRunner), new RspecConfig(path.resolve("./ruby"))) + }) + + test('no input, no output, no error', async function() { + let testItems: vscode.TestItem[] + expect(testItems = await loader["getBaseTestSuite"]([] as ParsedTest[])).to.not.throw + + expect(testItems).to.not.be.undefined + expect(testItems).to.have.length(0) + }); + + test('single file with one test case', async function() { + const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}]') + let testItems: vscode.TestItem[] + expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + + expect(testItems).to.not.be.undefined + expect(testItems).to.have.length(1) + testItemArrayMatches(testItems, [ + { + id: "abs_spec.rb", + file: "spec/abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + id: "abs_spec.rb[1:1]", + file: "spec/abs_spec.rb", + label: "finds the absolute value of 1", + line: 3 + } + ] + } + ]) + }) + + test('single file with two test cases', async function() { + const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11},{"id":"abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"abs_spec.rb","line_number":8,"type":null,"pending_message":null,"location":12}]') + let testItems: vscode.TestItem[] + expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + + expect(testItems).to.not.be.undefined + expect(testItems).to.have.length(1) + testItemArrayMatches(testItems, [ + { + id: "abs_spec.rb", + file: "spec/abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + id: "abs_spec.rb[1:1]", + file: "spec/abs_spec.rb", + label: "finds the absolute value of 1", + line: 3 + }, + { + id: "abs_spec.rb[1:2]", + file: "spec/abs_spec.rb", + label: "finds the absolute value of 0", + line: 7 + }, + ] + } + ]) + }) + + test('two files, one with a suite, each with one test case', async function() { + const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11},{"id":"square_spec.rb[1:1:1]","description":"finds the square of 2","full_description":"Square an unnecessary suite finds the square of 2","status":"passed","file_path":"square_spec.rb","line_number":5,"type":null,"pending_message":null,"location":111}]') + let testItems: vscode.TestItem[] + expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + + expect(testItems).to.not.be.undefined + expect(testItems).to.have.length(2) + testItemArrayMatches(testItems, [ + { + id: "abs_spec.rb", + file: "spec/abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + id: "abs_spec.rb[1:1]", + file: "spec/abs_spec.rb", + label: "finds the absolute value of 1", + line: 3 + } + ] + }, + { + id: "square_spec.rb", + file: "spec/square_spec.rb", + label: "square_spec.rb", + children: [ + { + id: "square_spec.rb[1:1:1]", + file: "spec/square_spec.rb", + label: "an unnecessary suite finds the square of 2", + line: 4 + }, + ] + } + ]) + }) + + // test('subfolder containing single file with one test case', async function() { + // const fakeRspecOutput = JSON.parse('[{"id":"foo/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"foo/abs_spec.rb","line_number":3,"type":null,"pending_message":null,"location":11}]') + // let testItems: vscode.TestItem[] + // expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + + // expect(testItems).to.not.be.undefined + // expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') + // testItemArrayMatches(testItems, [ + // { + // id: "foo", + // file: "spec/foo", + // label: "foo", + // children: [ + // { + // id: "foo/abs_spec.rb", + // file: "spec/foo/abs_spec.rb", + // label: "abs_spec.rb", + // children: [ + // { + // id: "foo/abs_spec.rb[1:1]", + // file: "spec/foo/abs_spec.rb", + // label: "finds the absolute value of 1", + // line: 2 + // } + // ] + // } + // ] + // } + // ]) + // }) + }) +}) \ No newline at end of file From d3283aa13ffd2a3fe84dc5d40358ef5f769a526f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 05:52:02 +0000 Subject: [PATCH 013/108] Update launch profiles --- .vscode/launch.json | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index b7a6aed..6415a66 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,10 +20,11 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/minitest/index", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/minitest" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"] + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "env": { "TEST_SUITE": "minitest" } }, { "name": "Run tests for RSpec", @@ -32,10 +33,24 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/frameworks/rspec/index", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/rspec" ], - "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"] + "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"], + "env": { "TEST_SUITE": "rspec" } + }, + { + "name": "Run unit tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", + "${workspaceFolder}/test/fixtures/unitTests" + ], + "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"], + "env": { "TEST_SUITE": "unitTests" } } ] } From 98d932d70d35d5e24e7f1d19d5fb247b8b28b469 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 05:52:50 +0000 Subject: [PATCH 014/108] Get Rspec success test passing --- src/main.ts | 4 +-- src/testRunContext.ts | 1 + src/testRunner.ts | 44 +++++++++++---------------- test/stubs/stubCancellationToken.ts | 25 ++++++++++++++++ test/stubs/stubTestController.ts | 45 ++++++++++++++++++++++++++++ test/stubs/stubTestItem.ts | 2 +- test/stubs/stubTestItemCollection.ts | 15 +++++++++- test/suite/helpers.ts | 23 ++++++++++++-- test/suite/index.ts | 3 +- test/suite/rspec/rspec.test.ts | 43 ++++++++++++++------------ 10 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 test/stubs/stubCancellationToken.ts create mode 100644 test/stubs/stubTestController.ts diff --git a/src/main.ts b/src/main.ts index e52e4c4..2611779 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,13 +76,13 @@ export async function activate(context: vscode.ExtensionContext) { controller.createRunProfile( 'Run', vscode.TestRunProfileKind.Run, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token, testConfig), + (request, token) => testLoaderFactory.getRunner().runHandler(request, token), true // Default run profile ); controller.createRunProfile( 'Debug', vscode.TestRunProfileKind.Debug, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token, testConfig, debuggerConfig) + (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig) ); } else { diff --git a/src/testRunContext.ts b/src/testRunContext.ts index a7ba907..a091022 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -151,6 +151,7 @@ export class TestRunContext { * @throws if test item could not be found */ public getTestItem(testId: string): vscode.TestItem { + testId = testId.replace(/^\.\/spec\//, '') let testItem = this.controller.items.get(testId) if (!testItem) { throw `Test not found on controller: ${testId}` diff --git a/src/testRunner.ts b/src/testRunner.ts index a92dc61..400c1c3 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -4,7 +4,6 @@ import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; -import { Config } from './config'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; @@ -163,7 +162,6 @@ export abstract class TestRunner implements vscode.Disposable { public async runHandler( request: vscode.TestRunRequest, token: vscode.CancellationToken, - config: Config, debuggerConfig?: vscode.DebugConfiguration ) { const context = new TestRunContext( @@ -171,7 +169,7 @@ export abstract class TestRunner implements vscode.Disposable { token, request, this.controller, - config, + this.config, debuggerConfig ) try { @@ -389,32 +387,26 @@ export abstract class TestRunner implements vscode.Disposable { * @returns Raw output from process */ protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { - let cancelUnsubscriber = context.token.onCancellationRequested( - (e: any) => { - this.log.debug("Cancellation requested") - this.killChild() - }, - this - ) - try { - const spawnArgs: childProcess.SpawnOptions = { - cwd: this.workspace?.uri.fsPath, - shell: true, - env: context.config.getProcessEnv() - }; + context.token.onCancellationRequested = () => { + this.log.debug("Cancellation requested") + this.killChild() + return {dispose: () => {}} + } - this.log.info(`Running command: ${testCommand}`); + const spawnArgs: childProcess.SpawnOptions = { + cwd: this.workspace?.uri.fsPath, + shell: true, + env: context.config.getProcessEnv() + }; - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); + this.log.info(`Running command: ${testCommand}`); - return await this.handleChildProcess(testProcess, context); - } - finally { - cancelUnsubscriber.dispose() - } + let testProcess = childProcess.spawn( + testCommand, + spawnArgs + ); + + return await this.handleChildProcess(testProcess, context); } /** diff --git a/test/stubs/stubCancellationToken.ts b/test/stubs/stubCancellationToken.ts new file mode 100644 index 0000000..a0d1c6d --- /dev/null +++ b/test/stubs/stubCancellationToken.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode' + +export class StubCancellationToken implements vscode.CancellationToken { + isCancellationRequested: boolean; + + private readonly eventEmitter = new vscode.EventEmitter(); + private listeners: ((e: any) => any)[] = [] + + constructor() { + this.isCancellationRequested = false + } + + public dispose() { + this.listeners = [] + } + + set onCancellationRequested(listener: vscode.Event) { + this.eventEmitter.event(listener) + } + + public cancel() { + this.isCancellationRequested = true + this.listeners.forEach(listener => listener(null)); + } +} \ No newline at end of file diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts new file mode 100644 index 0000000..97483e5 --- /dev/null +++ b/test/stubs/stubTestController.ts @@ -0,0 +1,45 @@ +import * as vscode from 'vscode' +import { StubTestItemCollection } from './stubTestItemCollection'; +import { StubTestItem } from './stubTestItem'; +import { instance, mock } from 'ts-mockito'; + +export class StubTestController implements vscode.TestController { + id: string = "stub_test_controller_id"; + label: string = "stub_test_controller_label"; + items: vscode.TestItemCollection = new StubTestItemCollection(); + mockTestRun: vscode.TestRun | undefined + + createRunProfile( + label: string, + kind: vscode.TestRunProfileKind, + runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => void | Thenable, + isDefault?: boolean, + tag?: vscode.TestTag + ): vscode.TestRunProfile { + return instance(mock()) + } + + resolveHandler?: ((item: vscode.TestItem | undefined) => void | Thenable) | undefined; + + createTestRun( + request: vscode.TestRunRequest, + name?: string, + persist?: boolean + ): vscode.TestRun { + this.mockTestRun = mock() + return instance(this.mockTestRun) + } + + createTestItem(id: string, label: string, uri?: vscode.Uri): vscode.TestItem { + return new StubTestItem(id, label, uri) + } + + dispose = () => {} + + getMockTestRun(): vscode.TestRun { + if (this.mockTestRun) + return this.mockTestRun + throw new Error("No test run") + } + +} \ No newline at end of file diff --git a/test/stubs/stubTestItem.ts b/test/stubs/stubTestItem.ts index b3c61a8..03c4bd1 100644 --- a/test/stubs/stubTestItem.ts +++ b/test/stubs/stubTestItem.ts @@ -18,7 +18,7 @@ export class StubTestItem implements vscode.TestItem { this.id = id this.label = label this.uri = uri - this.children = new StubTestItemCollection + this.children = new StubTestItemCollection() this.tags = [] this.canResolveChildren = false this.busy = false diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index 5d16bb3..adbdf08 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -36,6 +36,19 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } get(itemId: string): vscode.TestItem | undefined { - return this.data[this.testIds[itemId]] + let item: vscode.TestItem | undefined = undefined + this.data.forEach((child) => { + if (!item) { + if (child.id === itemId) { + item = child + } else { + let result = child.children.get(itemId) + if (result) { + item = result + } + } + } + }) + return item } } \ No newline at end of file diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 1609c5c..8bcd3a5 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { expect } from 'chai' import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; -import { anyString, anything, mock, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, when } from 'ts-mockito'; const dirPath = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri @@ -86,7 +86,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) expect(testItem.uri).to.not.be.undefined - expect(testItem.uri).to.eql(vscode.Uri.joinPath(dirPath, expectation.file), `uri mismatch (id: ${expectation.id})`) + expect(testItem.uri?.path).to.eql(vscode.Uri.joinPath(dirPath, expectation.file).path, `uri mismatch (id: ${expectation.id})`) if (expectation.children && expectation.children.length > 0) { expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) let i = 0; @@ -153,4 +153,23 @@ export function setupMockTestController(): vscode.TestController { let testItems = new StubTestItemCollection() when(mockTestController.items).thenReturn(testItems) return mockTestController +} + +export function setupMockRequest(testController: vscode.TestController, testId: string): vscode.TestRunRequest { + let mockRequest = mock() + let testItem = testController.items.get(testId) + if (testItem === undefined) { + throw new Error("Couldn't find test") + } + when(mockRequest.include).thenReturn([testItem]) + when(mockRequest.exclude).thenReturn([]) + return mockRequest +} + +export function getMockCancellationToken(): vscode.CancellationToken { + let mockToken = mock() + when(mockToken.isCancellationRequested).thenReturn(false) + when(mockToken.onCancellationRequested(anything(), anything(), undefined)).thenReturn({ dispose: () => {} }) + when(mockToken.onCancellationRequested(anything(), anything(), anything())).thenReturn({ dispose: () => {} }) + return instance(mockToken) } \ No newline at end of file diff --git a/test/suite/index.ts b/test/suite/index.ts index 946d782..3bda33e 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -9,7 +9,8 @@ export function run(): Promise { color: true, diff: true, bail: false, - fullTrace: true + fullTrace: true, + timeout: 10000, }); const suite = process.env['TEST_SUITE'] ?? '' diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index d79c1e4..4a982c7 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,14 +1,16 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path' -import { instance, reset } from 'ts-mockito' -import { setupMockTestController, stdout_logger, testItemCollectionMatches } from '../helpers'; +import { capture, instance } from 'ts-mockito' +import { setupMockRequest, stdout_logger, testItemCollectionMatches, TestItemExpectation, testItemMatches } from '../helpers'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; +//import { expect } from 'chai'; +import { StubTestController } from '../../stubs/stubTestController'; +import { StubCancellationToken } from '../../stubs/stubCancellationToken'; suite('Extension Test for RSpec', function() { - let mockTestController: vscode.TestController let testController: vscode.TestController let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] @@ -16,17 +18,12 @@ suite('Extension Test for RSpec', function() { let testLoader: TestLoader; this.beforeEach(async function() { - mockTestController = setupMockTestController() - testController = instance(mockTestController) + testController = new StubTestController() let config = new RspecConfig(path.resolve("./ruby")) testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config) testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config); }) - this.afterEach(function () { - reset(mockTestController) - }) - test('Load all tests', async function() { await testLoader.loadAllTests() @@ -81,17 +78,25 @@ suite('Extension Test for RSpec', function() { }) test('run test success', async function() { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./spec/square_spec.rb') + await testLoader.loadAllTests() - // assert.deepStrictEqual( - // controller.testEvents['./spec/square_spec.rb[1:1]'], - // [ - // { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" }, - // { state: "passed", test: "./spec/square_spec.rb[1:1]", type: "test" } - // ] - // ) + let mockRequest = setupMockRequest(testController, "square_spec.rb") + let request = instance(mockRequest) + let token = new StubCancellationToken() + await testRunner.runHandler(request, token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectation: TestItemExpectation = { + id: "square_spec.rb[1:1]", + file: "./spec/square_spec.rb", + label: "finds the square of 2", + line: 3 + } + testItemMatches( + capture(mockTestRun.passed).first()[0], + expectation + ) }) test('run test failure', async function() { From df5aef06ffe896073962e5c6383352785a4d1197 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 13:21:30 +0000 Subject: [PATCH 015/108] Fix usage of cancellation token and typo --- package.json | 2 +- src/main.ts | 2 +- src/testLoader.ts | 2 +- src/testRunner.ts | 5 ++--- test/stubs/stubCancellationToken.ts | 25 ------------------------- test/suite/rspec/rspec.test.ts | 5 ++--- 6 files changed, 7 insertions(+), 34 deletions(-) delete mode 100644 test/stubs/stubCancellationToken.ts diff --git a/package.json b/package.json index 06087e7..7bcc079 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "icon": "img/ruby-test-explorer.png", "author": "Connor Shea ", "publisher": "connorshea", - "version": "0.9.0", + "version": "0.10.0", "license": "MIT", "homepage": "https://github.com/connorshea/vscode-ruby-test-adapter", "repository": { diff --git a/src/main.ts b/src/main.ts index 2611779..e9c9def 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,7 +34,7 @@ export async function activate(context: vscode.ExtensionContext) { level: "debug", // See LogLevel type in @vscode-logging/types for possible logLevels logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` logOutputChannel: vscode.window.createOutputChannel("Ruby Test Explorer log"), // OutputChannel for the logger - sourceLocationTracking: true, + sourceLocationTracking: false, logConsole: (extensionConfig.get('logPanel') as boolean) // define if messages should be logged to the consol }); if (vscode.workspace.workspaceFolders == undefined) { diff --git a/src/testLoader.ts b/src/testLoader.ts index 8e9aefc..d91183b 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -282,7 +282,7 @@ export class TestLoader implements vscode.Disposable { return const filename = document.uri.fsPath; - this.log.info(`${filename} was saved - checking if this effects ${this.workspace.uri.fsPath}`); + this.log.info(`${filename} was saved - checking if this affects ${this.workspace.uri.fsPath}`); if (filename.startsWith(this.workspace.uri.fsPath)) { let testDirectory = this.getTestDirectory(); diff --git a/src/testRunner.ts b/src/testRunner.ts index 400c1c3..b1fc879 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -387,11 +387,10 @@ export abstract class TestRunner implements vscode.Disposable { * @returns Raw output from process */ protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { - context.token.onCancellationRequested = () => { + context.token.onCancellationRequested(() => { this.log.debug("Cancellation requested") this.killChild() - return {dispose: () => {}} - } + }) const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, diff --git a/test/stubs/stubCancellationToken.ts b/test/stubs/stubCancellationToken.ts deleted file mode 100644 index a0d1c6d..0000000 --- a/test/stubs/stubCancellationToken.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as vscode from 'vscode' - -export class StubCancellationToken implements vscode.CancellationToken { - isCancellationRequested: boolean; - - private readonly eventEmitter = new vscode.EventEmitter(); - private listeners: ((e: any) => any)[] = [] - - constructor() { - this.isCancellationRequested = false - } - - public dispose() { - this.listeners = [] - } - - set onCancellationRequested(listener: vscode.Event) { - this.eventEmitter.event(listener) - } - - public cancel() { - this.isCancellationRequested = true - this.listeners.forEach(listener => listener(null)); - } -} \ No newline at end of file diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 4a982c7..9f5fcb0 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -8,7 +8,6 @@ import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; //import { expect } from 'chai'; import { StubTestController } from '../../stubs/stubTestController'; -import { StubCancellationToken } from '../../stubs/stubCancellationToken'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -82,8 +81,8 @@ suite('Extension Test for RSpec', function() { let mockRequest = setupMockRequest(testController, "square_spec.rb") let request = instance(mockRequest) - let token = new StubCancellationToken() - await testRunner.runHandler(request, token) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) let mockTestRun = (testController as StubTestController).getMockTestRun() From dc08baad291b3f80f651c2004bc682e4cbd484b5 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 13:46:58 +0000 Subject: [PATCH 016/108] Tweak logging calls in test loader --- src/testLoader.ts | 98 +++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index d91183b..107c0bf 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -45,52 +45,58 @@ export class TestLoader implements vscode.Disposable { */ public async loadAllTests(): Promise { this.log.info(`Loading Ruby tests (${this.config.frameworkName()})...`); - let output = await this.testRunner.initTests(); - this.log.debug('Passing raw output from dry-run into getJsonFromOutput', output); - output = TestRunner.getJsonFromOutput(output); - this.log.debug('Parsing the returnd JSON', output); - let testMetadata; try { - testMetadata = JSON.parse(output); - } catch (error) { - this.log.error(`JSON parsing failed`, error); - } - - let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; - - testMetadata.examples.forEach( - (test: ParsedTest) => { - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); - test.id = test.id.replace(this.config.getFrameworkTestDirectory(), '') - test.file_path = test.file_path.replace(this.config.getFrameworkTestDirectory(), '') - tests.push(test); - this.log.debug("Parsed test", test) + let output = await this.testRunner.initTests(); + + this.log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); + output = TestRunner.getJsonFromOutput(output); + this.log.debug(`Parsing the returnd JSON: ${output}`); + let testMetadata; + try { + testMetadata = JSON.parse(output); + } catch (error) { + this.log.error('JSON parsing failed', error); } - ); - this.log.debug("Test output parsed. Building test suite", tests) - let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); - - // // Sort the children of each test suite based on their location in the test tree. - // testSuite.forEach((suite: vscode.TestItem) => { - // // NOTE: This will only sort correctly if everything is nested at the same - // // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is - // // introduced, the location is generated as e.g. 1231, which won't - // // sort properly relative to everything else. - // (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { - // if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { - // let aLocation: number = this.getTestLocation(a as TestInfo); - // let bLocation: number = this.getTestLocation(b as TestInfo); - // return aLocation - bLocation; - // } else { - // return 0; - // } - // }) - // }); - - this.controller.items.replace(testSuite); + let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; + + testMetadata.examples.forEach( + (test: ParsedTest) => { + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let test_location_string: string = test_location_array.join(''); + test.location = parseInt(test_location_string); + test.id = test.id.replace(this.config.getFrameworkTestDirectory(), '') + test.file_path = test.file_path.replace(this.config.getFrameworkTestDirectory(), '') + tests.push(test); + this.log.debug("Parsed test", test) + } + ); + + this.log.debug("Test output parsed. Building test suite", tests) + let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); + + // // Sort the children of each test suite based on their location in the test tree. + // testSuite.forEach((suite: vscode.TestItem) => { + // // NOTE: This will only sort correctly if everything is nested at the same + // // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is + // // introduced, the location is generated as e.g. 1231, which won't + // // sort properly relative to everything else. + // (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { + // if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { + // let aLocation: number = this.getTestLocation(a as TestInfo); + // let bLocation: number = this.getTestLocation(b as TestInfo); + // return aLocation - bLocation; + // } else { + // return 0; + // } + // }) + // }); + + this.controller.items.replace(testSuite); + } catch (e: any) { + this.log.error("Failed to load tests", e) + return + } } /** @@ -149,11 +155,11 @@ export class TestLoader implements vscode.Disposable { // organize the files under those subdirectories. subdirectories.forEach((directory) => { let dirPath = path.join(this.getTestDirectory() ?? '', directory) - this.log.debug("dirPath", dirPath) + this.log.debug(`dirPath: ${dirPath}`) let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { return file.startsWith(dirPath); }); - this.log.debug(`Files in subdirectory:`, directory, uniqueFilesInDirectory) + this.log.debug(`Files in subdirectory (${directory}):`, uniqueFilesInDirectory) let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(dirPath)); //directoryTestSuite.description = directory @@ -225,7 +231,7 @@ export class TestLoader implements vscode.Disposable { this.log.debug("Building tests for file", currentFile) currentFileTests.forEach((test) => { - this.log.debug("Building test", test.id) + this.log.debug(`Building test: ${test.id}`) // RSpec provides test ids like "file_name.rb[1:2:3]". // This uses the digits at the end of the id to create // an array of numbers representing the location of the From 2c1771603aae0aeb46c8d597dbe138de48b91f51 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 21:04:14 +0000 Subject: [PATCH 017/108] Fix path handling and get unit tests green again --- .gitignore | 2 + src/testLoader.ts | 50 +++--- .../fixtures/rspec/spec/subfolder/foo_spec.rb | 7 + test/suite/helpers.ts | 7 +- test/suite/index.ts | 8 +- test/suite/rspec/rspec.test.ts | 19 +- test/suite/unitTests/testLoader.test.ts | 167 +++++++++++++----- 7 files changed, 171 insertions(+), 89 deletions(-) create mode 100644 test/fixtures/rspec/spec/subfolder/foo_spec.rb diff --git a/.gitignore b/.gitignore index a0d6d58..8e77041 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ out/ ruby/Gemfile.lock /.vscode-test *.vsix +.rbenv-gemsets +.ruby-version diff --git a/src/testLoader.ts b/src/testLoader.ts index 107c0bf..0aaca82 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -58,7 +58,7 @@ export class TestLoader implements vscode.Disposable { this.log.error('JSON parsing failed', error); } - let tests: Array<{ id: string; full_description: string; description: string; file_path: string; line_number: number; location: number; }> = []; + let tests: Array = []; testMetadata.examples.forEach( (test: ParsedTest) => { @@ -128,6 +128,8 @@ export class TestLoader implements vscode.Disposable { */ private async getBaseTestSuite(tests: ParsedTest[]): Promise { let testSuite: vscode.TestItem[] = [] + let testCount = 0 + let dirPath = this.getTestDirectory() ?? '.' // Create an array of all test files and then abuse Sets to make it unique. let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; @@ -138,6 +140,9 @@ export class TestLoader implements vscode.Disposable { // Remove the spec/ directory from all the file path. uniqueFiles.forEach((file) => { + if (file.startsWith(path.sep)) { + file = file.substring(1) + } splitFilesArray.push(file.split('/')); }); @@ -154,23 +159,26 @@ export class TestLoader implements vscode.Disposable { // A nested loop to iterate through the direct subdirectories of spec/ and then // organize the files under those subdirectories. subdirectories.forEach((directory) => { - let dirPath = path.join(this.getTestDirectory() ?? '', directory) - this.log.debug(`dirPath: ${dirPath}`) + let subDirPath = path.join(dirPath, directory) let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - return file.startsWith(dirPath); + let fullFilePath = path.resolve(subDirPath, file) + this.log.debug(`Checking to see if ${fullFilePath} is in dir ${subDirPath}`) + return fullFilePath.startsWith(subDirPath); }); this.log.debug(`Files in subdirectory (${directory}):`, uniqueFilesInDirectory) - let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(dirPath)); + let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(subDirPath)); //directoryTestSuite.description = directory // Get the sets of tests for each file in the current directory. uniqueFilesInDirectory.forEach((currentFile: string) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile, directory }); + let currentFileTestSuite = this.getTestSuiteForFile(tests, currentFile, dirPath); directoryTestSuite.children.add(currentFileTestSuite); + testCount += currentFileTestSuite.children.size + 1 }); testSuite.push(directoryTestSuite); + testCount++ }); // Sort test suite types alphabetically. @@ -183,45 +191,39 @@ export class TestLoader implements vscode.Disposable { this.log.debug(`Files in top directory:`, topDirectoryFiles) topDirectoryFiles.forEach((currentFile) => { - let currentFileTestSuite = this.getTestSuiteForFile({ tests, currentFile }); + let currentFileTestSuite = this.getTestSuiteForFile(tests, currentFile, dirPath); testSuite.push(currentFileTestSuite); + testCount += currentFileTestSuite.children.size + 1 }); + this.log.debug(`Returning ${testCount} test cases`) return testSuite; } /** * Get the tests in a given file. + * + * @param tests Parsed output from framework + * @param currentFile Name of the file we're checking for tests + * @param dirPath Full path to the root test folder */ - public getTestSuiteForFile( - { tests, currentFile, directory }: { - tests: Array<{ - id: string; - full_description: string; - description: string; - file_path: string; - line_number: number; - location: number; - }>; currentFile: string; directory?: string; - }): vscode.TestItem { + public getTestSuiteForFile(tests: Array, currentFile: string, dirPath: string): vscode.TestItem { let currentFileTests = tests.filter(test => { return test.file_path === currentFile }); - let currentFileLabel = directory - ? currentFile.replace(`${this.config.getFrameworkTestDirectory()}${directory}/`, '') - : currentFile.replace(this.config.getFrameworkTestDirectory(), ''); + let currentFileSplitName = currentFile.split(path.sep); + let currentFileLabel = currentFileSplitName[currentFileSplitName.length - 1] + this.log.debug(`currentFileLabel: ${currentFileLabel}`) let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - // Concatenation of "/Users/username/whatever/project_dir" and "./spec/path/here.rb", - // but with the latter's first character stripped. let testDir = this.getTestDirectory() if (!testDir) { this.log.fatal("No test folder configured or workspace folder open") throw new Error("Missing test folders") } - let currentFileAsAbsolutePath = path.join(testDir, currentFile); + let currentFileAsAbsolutePath = path.resolve(dirPath, currentFile); let currentFileTestSuite: vscode.TestItem = this.controller.createTestItem( currentFile, diff --git a/test/fixtures/rspec/spec/subfolder/foo_spec.rb b/test/fixtures/rspec/spec/subfolder/foo_spec.rb new file mode 100644 index 0000000..51f95f2 --- /dev/null +++ b/test/fixtures/rspec/spec/subfolder/foo_spec.rb @@ -0,0 +1,7 @@ +require "test_helper" + +describe Foo do + it "wibbles and wobbles" do + expect("foo").to.not eq("bar") + end +end diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 8bcd3a5..497cb75 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,14 +1,9 @@ import * as vscode from 'vscode' -import * as path from 'path'; import { expect } from 'chai' import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; import { anyString, anything, instance, mock, when } from 'ts-mockito'; -const dirPath = vscode.workspace.workspaceFolders - ? vscode.workspace.workspaceFolders[0].uri - : vscode.Uri.file(path.resolve('./')) - export function noop() {} const NOOP_LOGGER: IVSCodeExtLogger = { @@ -86,7 +81,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) expect(testItem.uri).to.not.be.undefined - expect(testItem.uri?.path).to.eql(vscode.Uri.joinPath(dirPath, expectation.file).path, `uri mismatch (id: ${expectation.id})`) + expect(testItem.uri?.path).to.eql(expectation.file, `uri mismatch (id: ${expectation.id})`) if (expectation.children && expectation.children.length > 0) { expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) let i = 0; diff --git a/test/suite/index.ts b/test/suite/index.ts index 3bda33e..3e454fa 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -36,11 +36,11 @@ export function run(): Promise { mocha.run(failures => { if (failures > 0) { printFailureCount(failures) - - // Failed tests doesn't mean we failed to _run_ the tests :) - success(); + error(`${failures} test failures`) + } else { + success() } - }); + }) } catch (err) { error(err); } diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 9f5fcb0..7031a82 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -6,7 +6,6 @@ import { setupMockRequest, stdout_logger, testItemCollectionMatches, TestItemExp import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -//import { expect } from 'chai'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -16,6 +15,8 @@ suite('Extension Test for RSpec', function() { let testRunner: RspecTestRunner; let testLoader: TestLoader; + const dirPath = vscode.workspace.workspaceFolders![0].uri.path + this.beforeEach(async function() { testController = new StubTestController() let config = new RspecConfig(path.resolve("./ruby")) @@ -29,24 +30,24 @@ suite('Extension Test for RSpec', function() { const testSuite = testController.items testItemCollectionMatches(testSuite, [ { - file: "spec/abs_spec.rb", + file: path.resolve(dirPath, "spec/abs_spec.rb"), id: "abs_spec.rb", label: "abs_spec.rb", children: [ { - file: "spec/abs_spec.rb", + file: path.resolve(dirPath, "spec/abs_spec.rb"), id: "abs_spec.rb[1:1]", label: "finds the absolute value of 1", line: 3, }, { - file: "spec/abs_spec.rb", + file: path.resolve(dirPath, "spec/abs_spec.rb"), id: "abs_spec.rb[1:2]", label: "finds the absolute value of 0", line: 7, }, { - file: "spec/abs_spec.rb", + file: path.resolve(dirPath, "spec/abs_spec.rb"), id: "abs_spec.rb[1:3]", label: "finds the absolute value of -1", line: 11, @@ -54,18 +55,18 @@ suite('Extension Test for RSpec', function() { ] }, { - file: "spec/square_spec.rb", + file: path.resolve(dirPath, "spec/square_spec.rb"), id: "square_spec.rb", label: "square_spec.rb", children: [ { - file: "spec/square_spec.rb", + file: path.resolve(dirPath, "spec/square_spec.rb"), id: "square_spec.rb[1:1]", label: "finds the square of 2", line: 3, }, { - file: "spec/square_spec.rb", + file: path.resolve(dirPath, "spec/square_spec.rb"), id: "square_spec.rb[1:2]", label: "finds the square of 3", line: 7, @@ -88,7 +89,7 @@ suite('Extension Test for RSpec', function() { let expectation: TestItemExpectation = { id: "square_spec.rb[1:1]", - file: "./spec/square_spec.rb", + file: path.resolve(dirPath, "./spec/square_spec.rb"), label: "finds the square of 2", line: 3 } diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 798a66c..5591e45 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -12,6 +12,22 @@ suite('TestLoader', function() { let mockTestRunner = mock() let loader:TestLoader + const rspecDir = path.resolve("./test/fixtures/rspec/spec") + const config = new RspecConfig(path.resolve("./ruby")) + const configWrapper: RspecConfig = { + frameworkName: config.frameworkName, + getTestCommand: config.getTestCommand, + getDebugCommand: config.getDebugCommand, + getTestCommandWithFilePattern: config.getTestCommandWithFilePattern, + getTestDirectory: () => rspecDir, + getCustomFormatterLocation: config.getCustomFormatterLocation, + testCommandWithFormatterAndDebugger: config.testCommandWithFormatterAndDebugger, + getProcessEnv: config.getProcessEnv, + getFrameworkTestDirectory: config.getFrameworkTestDirectory, + rubyScriptPath: config.rubyScriptPath, + getFilePattern: config.getFilePattern + } + suite('#getBaseTestSuite()', function() { this.beforeEach(function() { let spiedWorkspace = spy(vscode.workspace) @@ -20,7 +36,14 @@ suite('TestLoader', function() { section == "framework" ? "rspec" : undefined }} as vscode.WorkspaceConfiguration) - loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], instance(mockTestController), instance(mockTestRunner), new RspecConfig(path.resolve("./ruby"))) + + loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], instance(mockTestController), instance(mockTestRunner), configWrapper) + let loaderSpy = spy(loader) + when(loaderSpy["getTestDirectory"]()).thenReturn(rspecDir) + }) + + this.afterEach(function() { + loader.dispose() }) test('no input, no output, no error', async function() { @@ -32,21 +55,30 @@ suite('TestLoader', function() { }); test('single file with one test case', async function() { - const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}]') + let tests: ParsedTest[] = [ + { + id: "abs_spec.rb[1:1]", + full_description: "Abs finds the absolute value of 1", + description: "finds the absolute value of 1", + file_path: "abs_spec.rb", + line_number: 4, + location: 11, + } + ]; let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw expect(testItems).to.not.be.undefined expect(testItems).to.have.length(1) testItemArrayMatches(testItems, [ { id: "abs_spec.rb", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "abs_spec.rb", children: [ { id: "abs_spec.rb[1:1]", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "finds the absolute value of 1", line: 3 } @@ -56,27 +88,44 @@ suite('TestLoader', function() { }) test('single file with two test cases', async function() { - const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11},{"id":"abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"abs_spec.rb","line_number":8,"type":null,"pending_message":null,"location":12}]') + let tests: ParsedTest[] = [ + { + id: "abs_spec.rb[1:1]", + full_description: "Abs finds the absolute value of 1", + description: "finds the absolute value of 1", + file_path: "abs_spec.rb", + line_number: 4, + location: 11, + }, + { + id: "abs_spec.rb[1:2]", + full_description: "Abs finds the absolute value of 0", + description: "finds the absolute value of 0", + file_path: "abs_spec.rb", + line_number: 8, + location: 12, + } + ]; let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw expect(testItems).to.not.be.undefined expect(testItems).to.have.length(1) testItemArrayMatches(testItems, [ { id: "abs_spec.rb", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "abs_spec.rb", children: [ { id: "abs_spec.rb[1:1]", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "finds the absolute value of 1", line: 3 }, { id: "abs_spec.rb[1:2]", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "finds the absolute value of 0", line: 7 }, @@ -86,21 +135,38 @@ suite('TestLoader', function() { }) test('two files, one with a suite, each with one test case', async function() { - const fakeRspecOutput = JSON.parse('[{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11},{"id":"square_spec.rb[1:1:1]","description":"finds the square of 2","full_description":"Square an unnecessary suite finds the square of 2","status":"passed","file_path":"square_spec.rb","line_number":5,"type":null,"pending_message":null,"location":111}]') + let tests: ParsedTest[] = [ + { + id: "abs_spec.rb[1:1]", + full_description: "Abs finds the absolute value of 1", + description: "finds the absolute value of 1", + file_path: "abs_spec.rb", + line_number: 4, + location: 11, + }, + { + id: "square_spec.rb[1:1:1]", + full_description: "Square an unnecessary suite finds the square of 2", + description: "finds the square of 2", + file_path: "square_spec.rb", + line_number: 5, + location: 111, + } + ]; let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw + expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw expect(testItems).to.not.be.undefined expect(testItems).to.have.length(2) testItemArrayMatches(testItems, [ { id: "abs_spec.rb", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "abs_spec.rb", children: [ { id: "abs_spec.rb[1:1]", - file: "spec/abs_spec.rb", + file: path.join(rspecDir, "abs_spec.rb"), label: "finds the absolute value of 1", line: 3 } @@ -108,12 +174,12 @@ suite('TestLoader', function() { }, { id: "square_spec.rb", - file: "spec/square_spec.rb", + file: path.join(rspecDir, "square_spec.rb"), label: "square_spec.rb", children: [ { id: "square_spec.rb[1:1:1]", - file: "spec/square_spec.rb", + file: path.join(rspecDir, "square_spec.rb"), label: "an unnecessary suite finds the square of 2", line: 4 }, @@ -122,35 +188,44 @@ suite('TestLoader', function() { ]) }) - // test('subfolder containing single file with one test case', async function() { - // const fakeRspecOutput = JSON.parse('[{"id":"foo/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"foo/abs_spec.rb","line_number":3,"type":null,"pending_message":null,"location":11}]') - // let testItems: vscode.TestItem[] - // expect(testItems = await loader["getBaseTestSuite"](fakeRspecOutput)).to.not.throw - - // expect(testItems).to.not.be.undefined - // expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') - // testItemArrayMatches(testItems, [ - // { - // id: "foo", - // file: "spec/foo", - // label: "foo", - // children: [ - // { - // id: "foo/abs_spec.rb", - // file: "spec/foo/abs_spec.rb", - // label: "abs_spec.rb", - // children: [ - // { - // id: "foo/abs_spec.rb[1:1]", - // file: "spec/foo/abs_spec.rb", - // label: "finds the absolute value of 1", - // line: 2 - // } - // ] - // } - // ] - // } - // ]) - // }) + test('subfolder containing single file with one test case', async function() { + let tests: ParsedTest[] = [ + { + id: "subfolder/foo_spec.rb[1:1]", + full_description: "Foo wibbles and wobbles", + description: "wibbles and wobbles", + file_path: "subfolder/foo_spec.rb", + line_number: 3, + location: 11, + } + ]; + let testItems: vscode.TestItem[] + expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw + + expect(testItems).to.not.be.undefined + expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') + testItemArrayMatches(testItems, [ + { + id: "subfolder", + file: path.join(rspecDir, "subfolder"), + label: "subfolder", + children: [ + { + id: "subfolder/foo_spec.rb", + file: path.join(rspecDir, "subfolder", "foo_spec.rb"), + label: "foo_spec.rb", + children: [ + { + id: "subfolder/foo_spec.rb[1:1]", + file: path.join(rspecDir, "subfolder", "foo_spec.rb"), + label: "wibbles and wobbles", + line: 2 + } + ] + } + ] + } + ]) + }) }) }) \ No newline at end of file From 3e9afefd1e9ae8de5055d8d98315b24c7d4b65aa Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 23 Feb 2022 22:16:53 +0000 Subject: [PATCH 018/108] Get first two rspec integration tests back to green --- .vscode/launch.json | 4 +- src/config.ts | 16 ++--- src/testLoader.ts | 2 +- .../fixtures/rspec/spec/subfolder/foo_spec.rb | 2 +- test/suite/rspec/rspec.test.ts | 60 +++++++++++++++---- 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6415a66..fe17153 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,7 +36,7 @@ "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/rspec" ], - "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], "env": { "TEST_SUITE": "rspec" } }, { @@ -49,7 +49,7 @@ "--extensionTestsPath=${workspaceFolder}/out/test/suite", "${workspaceFolder}/test/fixtures/unitTests" ], - "outFiles": ["${workspaceFolder}/out/test/**/**/*.js"], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], "env": { "TEST_SUITE": "unitTests" } } ] diff --git a/src/config.ts b/src/config.ts index 3644c3d..84dfca4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,12 +17,12 @@ export abstract class Config { /** * Printable name of the test framework */ - public abstract frameworkName(): string + public abstract frameworkName(): string - /** - * Path in which to look for test files for the test framework in use - */ - public abstract getFrameworkTestDirectory(): string + /** + * Path in which to look for test files for the test framework in use + */ + public abstract getFrameworkTestDirectory(): string /** * Get the user-configured test file pattern. @@ -46,9 +46,9 @@ export abstract class Config { * * @return The env */ - public abstract getProcessEnv(): any + public abstract getProcessEnv(): any - public static getTestFramework(log: IVSCodeExtLogger): string { + public static getTestFramework(log: IVSCodeExtLogger): string { let testFramework: string = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('testFramework') || ''; // If the test framework is something other than auto, return the value. if (['rspec', 'minitest', 'none'].includes(testFramework)) { @@ -100,4 +100,4 @@ export abstract class Config { return 'none'; } } -} \ No newline at end of file +} diff --git a/src/testLoader.ts b/src/testLoader.ts index 0aaca82..2c8ca64 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -161,7 +161,7 @@ export class TestLoader implements vscode.Disposable { subdirectories.forEach((directory) => { let subDirPath = path.join(dirPath, directory) let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - let fullFilePath = path.resolve(subDirPath, file) + let fullFilePath = path.resolve(dirPath, file) this.log.debug(`Checking to see if ${fullFilePath} is in dir ${subDirPath}`) return fullFilePath.startsWith(subDirPath); }); diff --git a/test/fixtures/rspec/spec/subfolder/foo_spec.rb b/test/fixtures/rspec/spec/subfolder/foo_spec.rb index 51f95f2..779094a 100644 --- a/test/fixtures/rspec/spec/subfolder/foo_spec.rb +++ b/test/fixtures/rspec/spec/subfolder/foo_spec.rb @@ -1,6 +1,6 @@ require "test_helper" -describe Foo do +describe "Foo" do it "wibbles and wobbles" do expect("foo").to.not eq("bar") end diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 7031a82..d25bf09 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -11,15 +11,25 @@ import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] - + let config: RspecConfig let testRunner: RspecTestRunner; let testLoader: TestLoader; - const dirPath = vscode.workspace.workspaceFolders![0].uri.path + const dirPath = path.resolve("ruby") + let expectedPath = (file: string): string => { + return path.resolve( + dirPath, + '..', + 'test', + 'fixtures', + 'rspec', + 'spec', + file) + } this.beforeEach(async function() { testController = new StubTestController() - let config = new RspecConfig(path.resolve("./ruby")) + config = new RspecConfig(dirPath) testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config) testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config); }) @@ -28,26 +38,50 @@ suite('Extension Test for RSpec', function() { await testLoader.loadAllTests() const testSuite = testController.items - testItemCollectionMatches(testSuite, [ + + console.log(`testSuite: ${JSON.stringify(testSuite)}`) + + testItemCollectionMatches(testSuite, + [ + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb[1:1]", + label: "wibbles and wobbles", + line: 3, + } + ] + } + ] + }, { - file: path.resolve(dirPath, "spec/abs_spec.rb"), + file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", label: "abs_spec.rb", children: [ { - file: path.resolve(dirPath, "spec/abs_spec.rb"), + file: expectedPath("abs_spec.rb"), id: "abs_spec.rb[1:1]", label: "finds the absolute value of 1", line: 3, }, { - file: path.resolve(dirPath, "spec/abs_spec.rb"), + file: expectedPath("abs_spec.rb"), id: "abs_spec.rb[1:2]", label: "finds the absolute value of 0", line: 7, }, { - file: path.resolve(dirPath, "spec/abs_spec.rb"), + file: expectedPath("abs_spec.rb"), id: "abs_spec.rb[1:3]", label: "finds the absolute value of -1", line: 11, @@ -55,24 +89,24 @@ suite('Extension Test for RSpec', function() { ] }, { - file: path.resolve(dirPath, "spec/square_spec.rb"), + file: expectedPath("square_spec.rb"), id: "square_spec.rb", label: "square_spec.rb", children: [ { - file: path.resolve(dirPath, "spec/square_spec.rb"), + file: expectedPath("square_spec.rb"), id: "square_spec.rb[1:1]", label: "finds the square of 2", line: 3, }, { - file: path.resolve(dirPath, "spec/square_spec.rb"), + file: expectedPath("square_spec.rb"), id: "square_spec.rb[1:2]", label: "finds the square of 3", line: 7, } ] - } + }, ] ) }) @@ -89,7 +123,7 @@ suite('Extension Test for RSpec', function() { let expectation: TestItemExpectation = { id: "square_spec.rb[1:1]", - file: path.resolve(dirPath, "./spec/square_spec.rb"), + file: expectedPath("square_spec.rb"), label: "finds the square of 2", line: 3 } From 35179a68466f43abde9b349a52d23ff5f6908aae Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 24 Feb 2022 00:19:56 +0000 Subject: [PATCH 019/108] Test all calls to runContext.passed in rspec success test --- test/suite/helpers.ts | 1 + test/suite/rspec/rspec.test.ts | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 497cb75..ba2a2f3 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -109,6 +109,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem * @param expectation Array of {@link TestItemExpectation}s to compare to */ export function testItemArrayMatches(testItems: readonly vscode.TestItem[], expectation: TestItemExpectation[]) { + expect(testItems.length).to.eq(expectation.length) testItems.forEach((testItem: vscode.TestItem, i: number) => { testItemMatches(testItem, expectation[i]) }) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index d25bf09..61d5a2d 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,12 +1,13 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path' -import { capture, instance } from 'ts-mockito' -import { setupMockRequest, stdout_logger, testItemCollectionMatches, TestItemExpectation, testItemMatches } from '../helpers'; +import { anything, capture, instance, verify } from 'ts-mockito' +import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches } from '../helpers'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { StubTestController } from '../../stubs/stubTestController'; +//import { expect } from 'chai'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -121,16 +122,20 @@ suite('Extension Test for RSpec', function() { let mockTestRun = (testController as StubTestController).getMockTestRun() - let expectation: TestItemExpectation = { + let invocationArgs = capture(mockTestRun.passed) + let expectation = { id: "square_spec.rb[1:1]", file: expectedPath("square_spec.rb"), label: "finds the square of 2", line: 3 } - testItemMatches( - capture(mockTestRun.passed).first()[0], - expectation - ) + let invocationArg = (index: number): vscode.TestItem => (invocationArgs.byCallIndex(index)[0] as vscode.TestItem) + testItemMatches(invocationArg(0), expectation) + testItemMatches(invocationArg(1), expectation) + testItemMatches(invocationArg(2), expectation) + testItemMatches(invocationArg(3), expectation) + verify(mockTestRun.passed(anything(), undefined)).times(4) + // TODO: Why 4 times?? }) test('run test failure', async function() { From ec5471d042de2707cd215db43c2d93ad04020ad8 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 24 Feb 2022 01:36:50 +0000 Subject: [PATCH 020/108] Convert remaining rspec tests, and refactor them a bit --- test/suite/helpers.ts | 34 +++++++- test/suite/rspec/rspec.test.ts | 152 +++++++++++++++++---------------- 2 files changed, 112 insertions(+), 74 deletions(-) diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index ba2a2f3..43cdb27 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -2,7 +2,8 @@ import * as vscode from 'vscode' import { expect } from 'chai' import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; -import { anyString, anything, instance, mock, when } from 'ts-mockito'; +import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; +import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; export function noop() {} @@ -168,4 +169,33 @@ export function getMockCancellationToken(): vscode.CancellationToken { when(mockToken.onCancellationRequested(anything(), anything(), undefined)).thenReturn({ dispose: () => {} }) when(mockToken.onCancellationRequested(anything(), anything(), anything())).thenReturn({ dispose: () => {} }) return instance(mockToken) -} \ No newline at end of file +} + +/** + * Argument captors for test state reporting functions + * + * @param mockTestRun mock/spy of the vscode.TestRun used to report test states + * @returns + */ +export function testStateCaptors(mockTestRun: vscode.TestRun) { + let invocationArgs1 = (args: ArgCaptor1, index: number): vscode.TestItem => args.byCallIndex(index)[0] + let invocationArgs2 = (args: ArgCaptor2, index: number): { testItem: vscode.TestItem, duration: number | undefined } => { let abci = args.byCallIndex(index); return {testItem: abci[0], duration: abci[1]}} + let invocationArgs3 = (args: ArgCaptor3, index: number): { testItem: vscode.TestItem, message: vscode.TestMessage, duration: number | undefined } => { let abci = args.byCallIndex(index); return {testItem: abci[0], message: abci[1], duration: abci[2]}} + let captors = { + enqueuedArgs: capture(mockTestRun.enqueued), + erroredArgs: capture(mockTestRun.errored), + failedArgs: capture(mockTestRun.failed), + passedArgs: capture(mockTestRun.passed), + startedArgs: capture(mockTestRun.started), + skippedArgs: capture(mockTestRun.skipped) + } + return { + ...captors, + enqueuedArg: (index: number) => invocationArgs1(captors.enqueuedArgs, index), + erroredArg: (index: number) => invocationArgs3(captors.erroredArgs, index), + failedArg: (index: number) => invocationArgs3(captors.failedArgs, index), + passedArg: (index: number) => invocationArgs2(captors.passedArgs, index), + startedArg: (index: number) => invocationArgs1(captors.startedArgs, index), + skippedArg: (index: number) => invocationArgs1(captors.skippedArgs, index), + } +} diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 61d5a2d..4a4ea9b 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,13 +1,12 @@ -import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path' -import { anything, capture, instance, verify } from 'ts-mockito' -import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches } from '../helpers'; +import { anything, instance, verify } from 'ts-mockito' +import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { StubTestController } from '../../stubs/stubTestController'; -//import { expect } from 'chai'; +import { expect } from 'chai'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -122,90 +121,99 @@ suite('Extension Test for RSpec', function() { let mockTestRun = (testController as StubTestController).getMockTestRun() - let invocationArgs = capture(mockTestRun.passed) + let args = testStateCaptors(mockTestRun) let expectation = { id: "square_spec.rb[1:1]", file: expectedPath("square_spec.rb"), label: "finds the square of 2", line: 3 } - let invocationArg = (index: number): vscode.TestItem => (invocationArgs.byCallIndex(index)[0] as vscode.TestItem) - testItemMatches(invocationArg(0), expectation) - testItemMatches(invocationArg(1), expectation) - testItemMatches(invocationArg(2), expectation) - testItemMatches(invocationArg(3), expectation) + testItemMatches(args.passedArg(0)["testItem"], expectation) + testItemMatches(args.passedArg(1)["testItem"], expectation) + testItemMatches(args.passedArg(2)["testItem"], expectation) + testItemMatches(args.passedArg(3)["testItem"], expectation) verify(mockTestRun.passed(anything(), undefined)).times(4) // TODO: Why 4 times?? }) test('run test failure', async function() { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./spec/square_spec.rb') - - // assert.deepStrictEqual( - // controller.testEvents['./spec/square_spec.rb[1:2]'][0], - // { state: "failed", test: "./spec/square_spec.rb[1:2]", type: "test" } - // ) - - // const lastEvent = controller.testEvents['./spec/square_spec.rb[1:2]'][1] - // assert.strictEqual(lastEvent.state, "failed") - // assert.strictEqual(lastEvent.line, undefined) - // assert.strictEqual(lastEvent.tooltip, undefined) - // assert.strictEqual(lastEvent.description, undefined) - // assert.ok(lastEvent.message?.startsWith("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n")) - - // assert.strictEqual(lastEvent.decorations!.length, 1) - // const decoration = lastEvent.decorations![0] - // assert.strictEqual(decoration.line, 8) - // assert.strictEqual(decoration.file, undefined) - // assert.strictEqual(decoration.hover, undefined) - // assert.strictEqual(decoration.message, " expected: 9\n got: 6\n\n(compared using ==)\n") + await testLoader.loadAllTests() + + let mockRequest = setupMockRequest(testController, "square_spec.rb") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "square_spec.rb[1:2]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 3", + line: 7 + } + let failedArg = args.failedArg(0) + testItemMatches(failedArg["testItem"], expectation) + + expect(failedArg["message"].message).to.contain("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n") + expect(failedArg["message"].actualOutput).to.be.undefined + expect(failedArg["message"].expectedOutput).to.be.undefined + expect(failedArg["message"].location?.range.start).to.eq(8) // line number + expect(failedArg["message"].location?.uri.fsPath).to.eq(expectation.file) + verify(mockTestRun.started(anything())).times(3) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) }) test('run test error', async function() { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./spec/abs_spec.rb[1:2]') - - // assert.deepStrictEqual( - // controller.testEvents['./spec/abs_spec.rb[1:2]'][0], - // { state: "running", test: "./spec/abs_spec.rb[1:2]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./spec/abs_spec.rb[1:2]'][1], - // { state: "failed", test: "./spec/abs_spec.rb[1:2]", type: "test" } - // ) - - // const lastEvent = controller.testEvents['./spec/abs_spec.rb[1:2]'][2] - // assert.strictEqual(lastEvent.state, "failed") - // assert.strictEqual(lastEvent.line, undefined) - // assert.strictEqual(lastEvent.tooltip, undefined) - // assert.strictEqual(lastEvent.description, undefined) - // assert.ok(lastEvent.message?.startsWith("RuntimeError:\nAbs for zero is not supported")) - - // assert.strictEqual(lastEvent.decorations!.length, 1) - // const decoration = lastEvent.decorations![0] - // assert.strictEqual(decoration.line, 8) - // assert.strictEqual(decoration.file, undefined) - // assert.strictEqual(decoration.hover, undefined) - // assert.ok(decoration.message?.startsWith("Abs for zero is not supported")) + await testLoader.loadAllTests() + + let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:2]") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "abs_spec.rb[1:2]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 0", + line: 7, + } + let erroredArg = args.erroredArg(0) + testItemMatches(erroredArg["testItem"], expectation) + + expect(erroredArg["message"].message).to.contain("RuntimeError:\nAbs for zero is not supported") + expect(erroredArg["message"].actualOutput).to.be.undefined + expect(erroredArg["message"].expectedOutput).to.be.undefined + expect(erroredArg["message"].location?.range.start).to.eq(8) // line number + expect(erroredArg["message"].location?.uri.fsPath).to.eq(expectation.file) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) }) test('run test skip', async function() { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./spec/abs_spec.rb[1:3]') - - // assert.deepStrictEqual( - // controller.testEvents['./spec/abs_spec.rb[1:3]'][0], - // { state: "running", test: "./spec/abs_spec.rb[1:3]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./spec/abs_spec.rb[1:3]'][1], - // { state: "skipped", test: "./spec/abs_spec.rb[1:3]", type: "test" } - // ) + await testLoader.loadAllTests() + + let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:3]") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "abs_spec.rb[1:3]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of -1", + line: 11 + } + testItemMatches(args.startedArg(0), expectation) + testItemMatches(args.skippedArg(0), expectation) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.skipped(anything())).times(1) }) }); From a6703a92c8f1180443e2bf4d4c02a7807eeaad8b Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 24 Feb 2022 02:58:40 +0000 Subject: [PATCH 021/108] Get rspec tests all green \o/ --- src/rspec/rspecTestRunner.ts | 9 ++---- src/testRunContext.ts | 4 +-- src/testRunner.ts | 57 +++++++++++++++++----------------- test/suite/rspec/rspec.test.ts | 42 +++++++++++++++++-------- 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 089704b..ba2c237 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -107,14 +107,9 @@ export class RspecTestRunner extends TestRunner { filePath, (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, ) - } else if (test.status === "failed" && test.pending_message !== null) { + } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { // Handle pending test cases. - context.errored( - test.id, - test.pending_message, - test.file_path.replace('./', ''), - test.line_number - ) + context.skipped(test.id) } }; diff --git a/src/testRunContext.ts b/src/testRunContext.ts index a091022..ad87df2 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -62,7 +62,7 @@ export class TestRunContext { line: number, duration?: number | undefined ): void { - this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) let testMessage = new vscode.TestMessage(message) let testItem = this.getTestItem(testId) testMessage.location = new vscode.Location( @@ -88,7 +88,7 @@ export class TestRunContext { line: number, duration?: number | undefined ): void { - this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) let testMessage = new vscode.TestMessage(message) let testItem = this.getTestItem(testId) testMessage.location = new vscode.Location( diff --git a/src/testRunner.ts b/src/testRunner.ts index b1fc879..2f6a0a5 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -112,9 +112,10 @@ export abstract class TestRunner implements vscode.Disposable { */ handleChildProcess = async (process: childProcess.ChildProcess, context: TestRunContext) => new Promise((resolve, reject) => { this.currentChildProcess = process; + let childProcessLogger = this.log.getChildLogger({ label: `ChildProcess(${context.config.frameworkName()})` }) this.currentChildProcess.on('exit', () => { - this.log.info('Child process has exited. Sending test run finish event.'); + childProcessLogger.info('Child process has exited. Sending test run finish event.'); this.currentChildProcess = undefined; context.testRun.end() resolve('{}'); @@ -122,7 +123,7 @@ export abstract class TestRunner implements vscode.Disposable { this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); + childProcessLogger.debug(data); if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { this.debugCommandStartedResolver() } @@ -131,7 +132,7 @@ export abstract class TestRunner implements vscode.Disposable { // TODO: Parse test IDs, durations, and failure message(s) from data this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); - this.log.debug(`[CHILD PROCESS OUTPUT] ${data}`); + childProcessLogger.debug(data); if (data.startsWith('PASSED:')) { data = data.replace('PASSED: ', ''); context.passed(data) @@ -325,8 +326,6 @@ export abstract class TestRunner implements vscode.Disposable { this.log.debug(`Test count mismatch {${node.label}}. Expected ${node.children.size + 1}, ran ${tests.length}`) } - //this.testStatesEmitter.fire({ type: 'suite', suite: node.id, state: 'completed' }); - } else { if (node.uri !== undefined && node.range !== undefined) { context.started(node) @@ -371,12 +370,12 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test suite. */ - runTestFramework = async (testCommand: string, type: string, context: TestRunContext) => - new Promise(async (resolve, reject) => { - this.log.info(`Running test suite: ${type}`); + runTestFramework = async (testCommand: string, type: string, context: TestRunContext) => + new Promise(async (resolve, reject) => { + this.log.info(`Running test suite: ${type}`); - resolve(await this.spawnCancellableChild(testCommand, context)) - }); + resolve(await this.spawnCancellableChild(testCommand, context)) + }); /** * Spawns a child process to run a command, that will be killed @@ -459,24 +458,24 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test. */ - protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; - - /** - * Gets the command to run tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param context Test run context - * @return The raw output from running the tests. - */ - protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; - - /** - * Gets the command to run the full test suite for the current workspace. - * - * @param context Test run context - * @return The raw output from running the test suite. - */ - protected abstract getFullTestSuiteCommand(context: TestRunContext): string; + protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; + + /** + * Gets the command to run tests in a given file. + * + * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param context Test run context + * @return The raw output from running the tests. + */ + protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; + + /** + * Gets the command to run the full test suite for the current workspace. + * + * @param context Test run context + * @return The raw output from running the test suite. + */ + protected abstract getFullTestSuiteCommand(context: TestRunContext): string; /** * Handles test state based on the output returned by the test command. @@ -484,5 +483,5 @@ export abstract class TestRunner implements vscode.Disposable { * @param test The test that we want to handle. * @param context Test run context */ - protected abstract handleStatus(test: any, context: TestRunContext): void; + protected abstract handleStatus(test: any, context: TestRunContext): void; } diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 4a4ea9b..0e1d5ce 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -133,7 +133,6 @@ suite('Extension Test for RSpec', function() { testItemMatches(args.passedArg(2)["testItem"], expectation) testItemMatches(args.passedArg(3)["testItem"], expectation) verify(mockTestRun.passed(anything(), undefined)).times(4) - // TODO: Why 4 times?? }) test('run test failure', async function() { @@ -147,22 +146,31 @@ suite('Extension Test for RSpec', function() { let mockTestRun = (testController as StubTestController).getMockTestRun() let args = testStateCaptors(mockTestRun) + + // Initial failure event with no details + let firstCallArgs = args.failedArg(0) + expect(firstCallArgs.testItem).to.have.property("id", "square_spec.rb[1:2]") + expect(firstCallArgs.testItem).to.have.property("label", "finds the square of 3") + expect(firstCallArgs.message.location?.uri.fsPath).to.eq(expectedPath("square_spec.rb")) + + // Actual failure report let expectation = { id: "square_spec.rb[1:2]", file: expectedPath("square_spec.rb"), label: "finds the square of 3", line: 7 } - let failedArg = args.failedArg(0) + let failedArg = args.failedArg(1) testItemMatches(failedArg["testItem"], expectation) expect(failedArg["message"].message).to.contain("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n") expect(failedArg["message"].actualOutput).to.be.undefined expect(failedArg["message"].expectedOutput).to.be.undefined - expect(failedArg["message"].location?.range.start).to.eq(8) // line number + expect(failedArg["message"].location?.range.start.line).to.eq(8) expect(failedArg["message"].location?.uri.fsPath).to.eq(expectation.file) + verify(mockTestRun.started(anything())).times(3) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(4) }) test('run test error', async function() { @@ -176,22 +184,30 @@ suite('Extension Test for RSpec', function() { let mockTestRun = (testController as StubTestController).getMockTestRun() let args = testStateCaptors(mockTestRun) + + // Initial failure event with no details + let firstCallArgs = args.failedArg(0) + expect(firstCallArgs.testItem).to.have.property("id", "abs_spec.rb[1:2]") + expect(firstCallArgs.testItem).to.have.property("label", "finds the absolute value of 0") + expect(firstCallArgs.message.location?.uri.fsPath).to.eq(expectedPath("abs_spec.rb")) + + // Actual failure report let expectation = { id: "abs_spec.rb[1:2]", file: expectedPath("abs_spec.rb"), label: "finds the absolute value of 0", line: 7, } - let erroredArg = args.erroredArg(0) - testItemMatches(erroredArg["testItem"], expectation) - - expect(erroredArg["message"].message).to.contain("RuntimeError:\nAbs for zero is not supported") - expect(erroredArg["message"].actualOutput).to.be.undefined - expect(erroredArg["message"].expectedOutput).to.be.undefined - expect(erroredArg["message"].location?.range.start).to.eq(8) // line number - expect(erroredArg["message"].location?.uri.fsPath).to.eq(expectation.file) + let failedArg = args.failedArg(1) + testItemMatches(failedArg["testItem"], expectation) + + expect(failedArg["message"].message).to.match(/RuntimeError:\nAbs for zero is not supported/) + expect(failedArg["message"].actualOutput).to.be.undefined + expect(failedArg["message"].expectedOutput).to.be.undefined + expect(failedArg["message"].location?.range.start.line).to.eq(8) + expect(failedArg["message"].location?.uri.fsPath).to.eq(expectation.file) verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(2) }) test('run test skip', async function() { From 245d96de70233c837b9b80ecf991790e11e82ad5 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 24 Feb 2022 22:35:46 +0000 Subject: [PATCH 022/108] Tidy logging, fix minor issue with subfolders --- src/config.ts | 5 --- src/minitest/minitestConfig.ts | 9 +---- src/rspec/rspecConfig.ts | 15 +------ src/testLoader.ts | 53 ++++++++++++++----------- test/runFrameworkTests.ts | 18 +++++++-- test/suite/helpers.ts | 43 +++++++++++++++----- test/suite/unitTests/config.test.ts | 4 +- test/suite/unitTests/testLoader.test.ts | 1 - 8 files changed, 83 insertions(+), 65 deletions(-) diff --git a/src/config.ts b/src/config.ts index 84dfca4..ab89d81 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,11 +19,6 @@ export abstract class Config { */ public abstract frameworkName(): string - /** - * Path in which to look for test files for the test framework in use - */ - public abstract getFrameworkTestDirectory(): string - /** * Get the user-configured test file pattern. * diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index d1156c3..214e307 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -7,19 +7,14 @@ export class MinitestConfig extends Config { return "Minitest" } - public getFrameworkTestDirectory(): string { - return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) - || path.join('.', 'test'); - } - /** * Get the user-configured test directory, if there is one. * * @return The test directory */ public getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string); - return directory || './test/'; + return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) + || path.join('.', 'test'); } /** diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index d7a071f..0fc61b0 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -39,23 +39,12 @@ export class RspecConfig extends Config { * @return The RSpec command */ public getTestCommandWithFilePattern(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); + let command: string = this.getTestCommand() const dir = this.getTestDirectory().replace(/\/$/, ""); let pattern = this.getFilePattern().map(p => `${dir}/**/${p}`).join(',') - command = command || `bundle exec rspec` return `${command} --pattern '${pattern}'`; } - /** - * Get the user-configured test directory, if there is one. - * - * @return The spec directory - */ - getTestDirectory(): string { - let directory: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string); - return directory || './spec/'; - } - /** * Get the absolute path of the custom_formatter.rb file. * @@ -91,7 +80,7 @@ export class RspecConfig extends Config { }); } - public getFrameworkTestDirectory(): string { + public getTestDirectory(): string { let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string return configDir ?? `.${path.sep}spec`; } diff --git a/src/testLoader.ts b/src/testLoader.ts index 2c8ca64..392d5d5 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -44,18 +44,19 @@ export class TestLoader implements vscode.Disposable { * @return The full test suite. */ public async loadAllTests(): Promise { - this.log.info(`Loading Ruby tests (${this.config.frameworkName()})...`); + let log = this.log.getChildLogger({label:"loadAddTests"}) + log.info(`Loading Ruby tests (${this.config.frameworkName()})...`); try { let output = await this.testRunner.initTests(); - this.log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); + log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); output = TestRunner.getJsonFromOutput(output); - this.log.debug(`Parsing the returnd JSON: ${output}`); + log.debug(`Parsing the returnd JSON: ${output}`); let testMetadata; try { testMetadata = JSON.parse(output); } catch (error) { - this.log.error('JSON parsing failed', error); + log.error('JSON parsing failed', error); } let tests: Array = []; @@ -65,14 +66,14 @@ export class TestLoader implements vscode.Disposable { let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let test_location_string: string = test_location_array.join(''); test.location = parseInt(test_location_string); - test.id = test.id.replace(this.config.getFrameworkTestDirectory(), '') - test.file_path = test.file_path.replace(this.config.getFrameworkTestDirectory(), '') + test.id = test.id.replace(this.config.getTestDirectory(), '') + test.file_path = test.file_path.replace(this.config.getTestDirectory(), '') tests.push(test); - this.log.debug("Parsed test", test) + log.debug("Parsed test", test) } ); - this.log.debug("Test output parsed. Building test suite", tests) + log.debug("Test output parsed. Building test suite", tests) let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); // // Sort the children of each test suite based on their location in the test tree. @@ -94,7 +95,7 @@ export class TestLoader implements vscode.Disposable { this.controller.items.replace(testSuite); } catch (e: any) { - this.log.error("Failed to load tests", e) + log.error("Failed to load tests", e) return } } @@ -103,9 +104,7 @@ export class TestLoader implements vscode.Disposable { * Get the test directory based on the configuration value if there's a configured test framework. */ private getTestDirectory(): string | undefined { - let testDirectory = this.config.getFrameworkTestDirectory(); - - this.log.debug("testDirectory", testDirectory) + let testDirectory = this.config.getTestDirectory(); if (testDirectory === '' || !this.workspace) { return undefined; @@ -127,6 +126,7 @@ export class TestLoader implements vscode.Disposable { * @return The test suite root with its children. */ private async getBaseTestSuite(tests: ParsedTest[]): Promise { + let log = this.log.getChildLogger({ label: "getBaseTestSuite" }) let testSuite: vscode.TestItem[] = [] let testCount = 0 let dirPath = this.getTestDirectory() ?? '.' @@ -136,10 +136,13 @@ export class TestLoader implements vscode.Disposable { let splitFilesArray: Array = []; - this.log.debug("Building base test suite from files", uniqueFiles) + log.debug("Building base test suite from files", uniqueFiles) // Remove the spec/ directory from all the file path. uniqueFiles.forEach((file) => { + if (file.startsWith('.')) { + file = file.substring(1) + } if (file.startsWith(path.sep)) { file = file.substring(1) } @@ -153,8 +156,11 @@ export class TestLoader implements vscode.Disposable { subdirectories.push(splitFile[0]); } }); + if (subdirectories[0] === ".") { + subdirectories = subdirectories.slice(1) + } subdirectories = [...new Set(subdirectories)]; - this.log.debug("Found subdirectories:", subdirectories) + log.debug("Found subdirectories:", subdirectories) // A nested loop to iterate through the direct subdirectories of spec/ and then // organize the files under those subdirectories. @@ -162,10 +168,10 @@ export class TestLoader implements vscode.Disposable { let subDirPath = path.join(dirPath, directory) let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { let fullFilePath = path.resolve(dirPath, file) - this.log.debug(`Checking to see if ${fullFilePath} is in dir ${subDirPath}`) - return fullFilePath.startsWith(subDirPath); + log.debug(`Checking to see if ${fullFilePath} is in dir ${subDirPath}`) + return subDirPath === fullFilePath.substring(0, fullFilePath.lastIndexOf(path.sep)) }); - this.log.debug(`Files in subdirectory (${directory}):`, uniqueFilesInDirectory) + log.debug(`Files in subdirectory (${directory}):`, uniqueFilesInDirectory) let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(subDirPath)); //directoryTestSuite.description = directory @@ -188,7 +194,7 @@ export class TestLoader implements vscode.Disposable { let topDirectoryFiles = uniqueFiles.filter((filePath) => { return filePath.split('/').length === 1; }); - this.log.debug(`Files in top directory:`, topDirectoryFiles) + log.debug(`Files in top directory:`, topDirectoryFiles) topDirectoryFiles.forEach((currentFile) => { let currentFileTestSuite = this.getTestSuiteForFile(tests, currentFile, dirPath); @@ -196,7 +202,7 @@ export class TestLoader implements vscode.Disposable { testCount += currentFileTestSuite.children.size + 1 }); - this.log.debug(`Returning ${testCount} test cases`) + log.debug(`Returning ${testCount} test cases`) return testSuite; } @@ -208,19 +214,19 @@ export class TestLoader implements vscode.Disposable { * @param dirPath Full path to the root test folder */ public getTestSuiteForFile(tests: Array, currentFile: string, dirPath: string): vscode.TestItem { + let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${currentFile})` }) let currentFileTests = tests.filter(test => { return test.file_path === currentFile }); let currentFileSplitName = currentFile.split(path.sep); let currentFileLabel = currentFileSplitName[currentFileSplitName.length - 1] - this.log.debug(`currentFileLabel: ${currentFileLabel}`) let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); let testDir = this.getTestDirectory() if (!testDir) { - this.log.fatal("No test folder configured or workspace folder open") + log.fatal("No test folder configured or workspace folder open") throw new Error("Missing test folders") } let currentFileAsAbsolutePath = path.resolve(dirPath, currentFile); @@ -231,9 +237,8 @@ export class TestLoader implements vscode.Disposable { vscode.Uri.file(currentFileAsAbsolutePath) ); - this.log.debug("Building tests for file", currentFile) currentFileTests.forEach((test) => { - this.log.debug(`Building test: ${test.id}`) + log.debug(`Building test: ${test.id}`) // RSpec provides test ids like "file_name.rb[1:2:3]". // This uses the digits at the end of the id to create // an array of numbers representing the location of the @@ -276,7 +281,7 @@ export class TestLoader implements vscode.Disposable { * @param string The string to convert to PascalCase. * @return The converted string. */ - private snakeToPascalCase(string: string): string { + private snakeToPascalCase(string: string): string { if (string.includes('/')) { return string } return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); } diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 16a2f33..8f50a28 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -3,11 +3,23 @@ import { runTests, downloadAndUnzipVSCode } from '@vscode/test-electron'; const extensionDevelopmentPath = path.resolve(__dirname, '../../'); const allowedSuiteArguments = ["rspec", "minitest", "unitTests"] +const maxDownloadRetries = 5; async function main(framework: string) { - let vscodeExecutablePath = await downloadAndUnzipVSCode('stable') - - await runTestSuite(vscodeExecutablePath, framework) + let vscodeExecutablePath: string | undefined + for (let retries = 0; retries < maxDownloadRetries; retries++) { + try { + vscodeExecutablePath = await downloadAndUnzipVSCode('stable') + break + } catch (error) { + console.warn(`Failed to download Visual Studio Code (attempt ${retries} of ${maxDownloadRetries}): ${error}`) + } + } + if (vscodeExecutablePath) { + await runTestSuite(vscodeExecutablePath, framework) + } else { + console.error("crap") + } } /** diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 43cdb27..a0efd22 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -7,6 +7,9 @@ import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCa export function noop() {} +/** + * Noop logger for use in testing where logs are usually unnecessary + */ const NOOP_LOGGER: IVSCodeExtLogger = { changeLevel: noop, changeSourceLocationTracking: noop, @@ -43,6 +46,9 @@ function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogge } } +/** + * Logger that logs to stdout - not terribly pretty but useful for seeing what failing tests are doing + */ const STDOUT_LOGGER: IVSCodeExtLogger = { changeLevel: noop, changeSourceLocationTracking: noop, @@ -58,7 +64,15 @@ const STDOUT_LOGGER: IVSCodeExtLogger = { } Object.freeze(STDOUT_LOGGER) +/** + * Get a noop logger for use in testing where logs are usually unnecessary + */ export function noop_logger(): IVSCodeExtLogger { return NOOP_LOGGER } +/** + * Get a logger that logs to stdout. + * + * Not terribly pretty but useful for seeing what failing tests are doing + */ export function stdout_logger(): IVSCodeExtLogger { return STDOUT_LOGGER } /** @@ -66,8 +80,8 @@ export function stdout_logger(): IVSCodeExtLogger { return STDOUT_LOGGER } */ export type TestItemExpectation = { id: string, - file: string, label: string, + file?: string, line?: number, children?: TestItemExpectation[] } @@ -81,8 +95,12 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem if (!expectation) expect.fail("No expectation given") expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) - expect(testItem.uri).to.not.be.undefined - expect(testItem.uri?.path).to.eql(expectation.file, `uri mismatch (id: ${expectation.id})`) + if (expectation.file) { + expect(testItem.uri).to.not.be.undefined + expect(testItem.uri?.path).to.eql(expectation.file, `uri mismatch (id: ${expectation.id})`) + } else { + expect(testItem.uri).to.be.undefined + } if (expectation.children && expectation.children.length > 0) { expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) let i = 0; @@ -132,7 +150,7 @@ export function testItemArrayMatches(testItems: readonly vscode.TestItem[], expe export function setupMockTestController(): vscode.TestController { let mockTestController = mock() - let createTestItem = (id: string, label: string, uri: vscode.Uri | undefined) => { + let createTestItem = (id: string, label: string, uri?: vscode.Uri | undefined) => { return { id: id, label: label, @@ -146,19 +164,24 @@ export function setupMockTestController(): vscode.TestController { children: new StubTestItemCollection(), } } + when(mockTestController.createTestItem(anyString(), anyString())).thenCall(createTestItem) when(mockTestController.createTestItem(anyString(), anyString(), anything())).thenCall(createTestItem) let testItems = new StubTestItemCollection() when(mockTestController.items).thenReturn(testItems) return mockTestController } -export function setupMockRequest(testController: vscode.TestController, testId: string): vscode.TestRunRequest { +export function setupMockRequest(testController: vscode.TestController, testId?: string): vscode.TestRunRequest { let mockRequest = mock() - let testItem = testController.items.get(testId) - if (testItem === undefined) { - throw new Error("Couldn't find test") + if (testId) { + let testItem = testController.items.get(testId) + if (testItem === undefined) { + throw new Error("Couldn't find test") + } + when(mockRequest.include).thenReturn([testItem]) + } else { + when(mockRequest.include).thenReturn([]) } - when(mockRequest.include).thenReturn([testItem]) when(mockRequest.exclude).thenReturn([]) return mockRequest } @@ -175,7 +198,7 @@ export function getMockCancellationToken(): vscode.CancellationToken { * Argument captors for test state reporting functions * * @param mockTestRun mock/spy of the vscode.TestRun used to report test states - * @returns + * @returns A map of argument captors for test state reporting functions */ export function testStateCaptors(mockTestRun: vscode.TestRun) { let invocationArgs1 = (args: ArgCaptor1, index: number): vscode.TestItem => args.byCallIndex(index)[0] diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index b6ab67f..43460a0 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -58,10 +58,10 @@ suite('Config', function() { .eq("bundle exec rspec --pattern './spec/**/*_test.rb,./spec/**/test_*.rb'") }) - suite("#getFrameworkTestDirectory()", function() { + suite("#getTestDirectory()", function() { test("with no config set, it returns ./spec", function() { let config = new RspecConfig("../../../ruby") - expect(config.getFrameworkTestDirectory()).to.eq("./spec") + expect(config.getTestDirectory()).to.eq("./spec") }) }) }) diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 5591e45..6a6bf4a 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -23,7 +23,6 @@ suite('TestLoader', function() { getCustomFormatterLocation: config.getCustomFormatterLocation, testCommandWithFormatterAndDebugger: config.testCommandWithFormatterAndDebugger, getProcessEnv: config.getProcessEnv, - getFrameworkTestDirectory: config.getFrameworkTestDirectory, rubyScriptPath: config.rubyScriptPath, getFilePattern: config.getFilePattern } From e4ac041094b796b2a7595d08c693396a558eb274 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 25 Feb 2022 12:11:30 +0000 Subject: [PATCH 023/108] Change test loading to be more lazy and fill suite with files quickly on startup using glob I can actually run tests with it and have it go green again \o/ --- src/main.ts | 8 +-- src/testLoader.ts | 43 ++++++++++++++++ src/testRunContext.ts | 116 ++++++++++++++++++++++++++++++------------ src/testRunner.ts | 97 ++++++++++++++++++----------------- 4 files changed, 181 insertions(+), 83 deletions(-) diff --git a/src/main.ts b/src/main.ts index e9c9def..a0a6cd8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,15 +62,15 @@ export async function activate(context: vscode.ExtensionContext) { const testLoaderFactory = new TestFactory(log, workspace, controller, testConfig); context.subscriptions.push(controller); - testLoaderFactory.getLoader().loadAllTests(); + testLoaderFactory.getLoader().buildInitialTestItems(); // TODO: Allow lazy-loading of child tests - below is taken from example in docs // Custom handler for loading tests. The "test" argument here is undefined, // but if we supported lazy-loading child test then this could be called with // the test whose children VS Code wanted to load. - // controller.resolveHandler = test => { - // controller.items.replace([]); - // }; + controller.resolveHandler = test => { + log.debug('resolveHandler called', test) + }; // TODO: (?) Add a "Profile" profile for profiling tests controller.createRunProfile( diff --git a/src/testLoader.ts b/src/testLoader.ts index 392d5d5..05e4e90 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import glob from 'glob'; import * as path from 'path'; import { IChildLogger } from '@vscode-logging/logger'; import { TestRunner } from './testRunner'; @@ -37,6 +38,48 @@ export class TestLoader implements vscode.Disposable { this.disposables = []; } + buildInitialTestItems() { + let log = this.log.getChildLogger({ label: `${this.buildInitialTestItems.name}` }) + let testDir = path.resolve(this.workspace?.uri.fsPath ?? '.', this.config.getTestDirectory()) + let testGlob = path.join('**', `+(${this.config.getFilePattern().join('|')})`) + log.info(`Looking for test files in ${testDir} using glob patterns`, this.config.getFilePattern()) + + glob(testGlob, { cwd: testDir }, (err, files) => { + if (err) { + log.error("Error searching for test files", err); + } + + // Add files to the test suite + files.forEach(f => { + let fileSegments = f.split(path.sep) + let id:string = '' + let collection: vscode.TestItemCollection = this.controller.items + for (let i = 0; i < fileSegments.length; i++) { + id = path.join(id, fileSegments[i]) + let testItem: vscode.TestItem | undefined = collection.get(id) + if (testItem) { + collection = testItem.children + continue + } else { + let fPath = path.resolve(testDir, f) + + testItem = this.controller.createTestItem(id, fileSegments[i], vscode.Uri.file(fPath)) + if(fileSegments[i].includes('.')) { + // If a file and not a dir, allow to resolve the tests inside + log.debug(`Adding test item for file ${fPath}`) + testItem.canResolveChildren = true + } else { + log.debug(`Adding test item for folder ${fPath}`) + } + // testItem.busy = true Maybe? + collection.add(testItem) + collection = testItem.children + } + } + }); + }); + } + /** * Takes the output from initTests() and parses the resulting * JSON into a TestSuiteInfo object. diff --git a/src/testRunContext.ts b/src/testRunContext.ts index ad87df2..9aaef9b 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode' +import path from 'path' import { IChildLogger } from '@vscode-logging/logger' import { Config } from './config' @@ -37,13 +38,17 @@ export class TestRunContext { */ public enqueued(test: string | vscode.TestItem): void { if (typeof test === "string") { - this.log.debug(`Enqueued: ${test}`) - this.testRun.enqueued(this.getTestItem(test)) - } - else { - this.log.debug(`Enqueued: ${test.id}`) - this.testRun.enqueued(test) - } + try { + this.testRun.enqueued(this.getTestItem(test)) + this.log.debug(`Enqueued: ${test}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Enqueued`, e) + } + } + else { + this.testRun.enqueued(test) + this.log.debug(`Enqueued: ${test.id}`) + } } /** @@ -62,14 +67,18 @@ export class TestRunContext { line: number, duration?: number | undefined ): void { - this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) let testMessage = new vscode.TestMessage(message) - let testItem = this.getTestItem(testId) - testMessage.location = new vscode.Location( - testItem.uri ?? vscode.Uri.file(file), - new vscode.Position(line, 0) - ) - this.testRun.errored(testItem, testMessage, duration) + try { + let testItem = this.getTestItem(testId) + testMessage.location = new vscode.Location( + testItem.uri ?? vscode.Uri.file(file), + new vscode.Position(line, 0) + ) + this.testRun.errored(testItem, testMessage, duration) + this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Errored`, e) + } } /** @@ -88,14 +97,18 @@ export class TestRunContext { line: number, duration?: number | undefined ): void { - this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) - let testMessage = new vscode.TestMessage(message) - let testItem = this.getTestItem(testId) - testMessage.location = new vscode.Location( - testItem.uri ?? vscode.Uri.file(file), - new vscode.Position(line, 0) - ) - this.testRun.failed(testItem, testMessage, duration) + try { + let testMessage = new vscode.TestMessage(message) + let testItem = this.getTestItem(testId) + testMessage.location = new vscode.Location( + testItem.uri ?? vscode.Uri.file(file), + new vscode.Position(line, 0) + ) + this.testRun.failed(testItem, testMessage, duration) + this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Failed`, e) + } } /** @@ -108,8 +121,12 @@ export class TestRunContext { testId: string, duration?: number | undefined ): void { - this.log.debug(`Passed: ${testId}${duration ? `, duration: ${duration}ms` : ''}`) - this.testRun.passed(this.getTestItem(testId), duration) + try { + this.testRun.passed(this.getTestItem(testId), duration) + this.log.debug(`Passed: ${testId}${duration ? `, duration: ${duration}ms` : ''}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Passed`, e) + } } /** @@ -119,12 +136,16 @@ export class TestRunContext { */ public skipped(test: string | vscode.TestItem): void { if (typeof test === "string") { - this.log.debug(`Skipped: ${test}`) - this.testRun.skipped(this.getTestItem(test)) + try { + this.testRun.skipped(this.getTestItem(test)) + this.log.debug(`Skipped: ${test}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Skipped`, e) + } } else { - this.log.debug(`Skipped: ${test.id}`) this.testRun.skipped(test) + this.log.debug(`Skipped: ${test.id}`) } } @@ -135,12 +156,16 @@ export class TestRunContext { */ public started(test: string | vscode.TestItem): void { if (typeof test === "string") { - this.log.debug(`Started: ${test}`) - this.testRun.started(this.getTestItem(test)) + try { + this.testRun.started(this.getTestItem(test)) + this.log.debug(`Started: ${test}`) + } catch (e: any) { + this.log.error(`Failed to set test ${test} as Started`, e) + } } else { - this.log.debug(`Started: ${test.id}`) this.testRun.started(test) + this.log.debug(`Started: ${test.id}`) } } @@ -151,10 +176,37 @@ export class TestRunContext { * @throws if test item could not be found */ public getTestItem(testId: string): vscode.TestItem { + let log = this.log.getChildLogger({label: `${this.getTestItem.name}(${testId})`}) testId = testId.replace(/^\.\/spec\//, '') - let testItem = this.controller.items.get(testId) + let idSegments = testId.split(path.sep) + let collection: vscode.TestItemCollection = this.controller.items + + // Walk the test hierarchy to find the collection containing our test file + for (let i = 0; i < idSegments.length - 1; i++) { + let collectionId = (i == 0) + ? idSegments[0] + : idSegments.slice(0,i).join(path.sep) + let childCollection = collection.get(collectionId)?.children + if (!childCollection) { + throw `Test collection not found: ${collectionId}` + } + collection = childCollection + } + + // Need to make sure we strip locations from file id to get final collection + let fileId = testId.replace(/\[[0-9](?::[0-9])*\]$/, '') + let childCollection = collection.get(fileId)?.children + if (!childCollection) { + throw `Test collection not found: ${fileId}` + } + collection = childCollection + log.debug("Got parent collection, looking for test") + + let testItem = collection.get(testId) if (!testItem) { - throw `Test not found on controller: ${testId}` + // Create a basic test item with what little info we have to be filled in later + testItem = this.controller.createTestItem(testId, idSegments[idSegments.length - 1], vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId))); + collection.add(testItem); } return testItem } diff --git a/src/testRunner.ts b/src/testRunner.ts index 2f6a0a5..1624c95 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -117,7 +117,6 @@ export abstract class TestRunner implements vscode.Disposable { this.currentChildProcess.on('exit', () => { childProcessLogger.info('Child process has exited. Sending test run finish event.'); this.currentChildProcess = undefined; - context.testRun.end() resolve('{}'); }); @@ -286,62 +285,66 @@ export abstract class TestRunner implements vscode.Disposable { ): Promise { // Special case handling for the root suite, since it can be run // with runFullTestSuite() - if (node == null) { - this.controller.items.forEach((testSuite) => { - this.enqueTestAndChildren(testSuite, context) - }) - let testOutput = await this.runFullTestSuite(context); - testOutput = TestRunner.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string; }) => { - this.handleStatus(test, context); - }); - } - // If the suite is a file, run the tests as a file rather than as separate tests. - } else if (node.label.endsWith('.rb')) { - // Mark selected tests as enqueued - this.enqueTestAndChildren(node, context) - - context.started(node) - let testOutput = await this.runTestFile(`${node.uri?.fsPath}`, context); - - testOutput = TestRunner.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string }) => { - this.handleStatus(test, context); - }); - } + try { + if (node == null) { + this.controller.items.forEach((testSuite) => { + this.enqueTestAndChildren(testSuite, context) + }) + let testOutput = await this.runFullTestSuite(context); + testOutput = TestRunner.getJsonFromOutput(testOutput); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; - if (tests.length != node.children.size + 1) { - this.log.debug(`Test count mismatch {${node.label}}. Expected ${node.children.size + 1}, ran ${tests.length}`) - } + if (tests && tests.length > 0) { + tests.forEach((test: { id: string; }) => { + this.handleStatus(test, context); + }); + } + // If the suite is a file, run the tests as a file rather than as separate tests. + } else if (node.label.endsWith('.rb')) { + // Mark selected tests as enqueued + this.enqueTestAndChildren(node, context) - } else { - if (node.uri !== undefined && node.range !== undefined) { context.started(node) - - // Run the test at the given line, add one since the line is 0-indexed in - // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(`${node.uri.fsPath}:${node.range?.end.line}`, context); + let testOutput = await this.runTestFile(`${node.uri?.fsPath}`, context); testOutput = TestRunner.getJsonFromOutput(testOutput); this.log.debug('Parsing the below JSON:'); this.log.debug(`${testOutput}`); let testMetadata = JSON.parse(testOutput); - let currentTest = testMetadata.examples[0]; + let tests: Array = testMetadata.examples; + + if (tests && tests.length > 0) { + tests.forEach((test: { id: string }) => { + this.handleStatus(test, context); + }); + } + + if (tests.length != node.children.size + 1) { + this.log.debug(`Test count mismatch {${node.label}}. Expected ${node.children.size + 1}, ran ${tests.length}`) + } - this.handleStatus(currentTest, context); + } else { + if (node.uri !== undefined && node.range !== undefined) { + context.started(node) + + // Run the test at the given line, add one since the line is 0-indexed in + // VS Code and 1-indexed for RSpec/Minitest. + let testOutput = await this.runSingleTest(`${node.uri.fsPath}:${node.range?.end.line}`, context); + + testOutput = TestRunner.getJsonFromOutput(testOutput); + this.log.debug('Parsing the below JSON:'); + this.log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let currentTest = testMetadata.examples[0]; + + this.handleStatus(currentTest, context); + } } + } finally { + context.testRun.end() } } From ffc699bc927c1a7c3f3a8ae014a7c1291e27bdc9 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 28 Feb 2022 17:18:51 +0000 Subject: [PATCH 024/108] Fully implement lazy test loading for rspec --- package-lock.json | 4 +- src/main.ts | 11 +- src/minitest/minitestTestRunner.ts | 13 +- src/rspec/rspecTestRunner.ts | 16 +- src/testFactory.ts | 12 +- src/testLoader.ts | 278 ++++----------- src/testRunContext.ts | 133 ++----- src/testRunner.ts | 19 +- src/testSuite.ts | 92 +++++ test/suite/rspec/rspec.test.ts | 113 +++++- test/suite/unitTests/testLoader.test.ts | 452 ++++++++++++------------ 11 files changed, 560 insertions(+), 583 deletions(-) create mode 100644 src/testSuite.ts diff --git a/package-lock.json b/package-lock.json index 0364673..6a46bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-ruby-test-adapter", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-ruby-test-adapter", - "version": "0.9.0", + "version": "0.10.0", "license": "MIT", "dependencies": { "@vscode-logging/logger": "^1.2.3", diff --git a/src/main.ts b/src/main.ts index a0a6cd8..bb0f465 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,14 +62,21 @@ export async function activate(context: vscode.ExtensionContext) { const testLoaderFactory = new TestFactory(log, workspace, controller, testConfig); context.subscriptions.push(controller); - testLoaderFactory.getLoader().buildInitialTestItems(); + testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); // TODO: Allow lazy-loading of child tests - below is taken from example in docs // Custom handler for loading tests. The "test" argument here is undefined, // but if we supported lazy-loading child test then this could be called with // the test whose children VS Code wanted to load. - controller.resolveHandler = test => { + controller.resolveHandler = async test => { log.debug('resolveHandler called', test) + // TODO: Glob child files and folders regardless of canResolveChildren + // TODO: If canResolveChildren, dry run test and update TestItems with response + if (!test) { + await testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); + } else { + await testLoaderFactory.getLoader().parseTestsInFile(test); + } }; // TODO: (?) Add a "Profile" profile for profiling tests diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index bcec035..6915b70 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -11,7 +11,7 @@ export class MinitestTestRunner extends TestRunner { * * @return The raw output from the Minitest JSON formatter. */ - initTests = async () => new Promise((resolve, reject) => { + initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { let cmd = `${this.getTestCommand()} vscode:minitest:list`; // Allow a buffer of 64MB. @@ -21,6 +21,10 @@ export class MinitestTestRunner extends TestRunner { env: this.config.getProcessEnv() }; + testItems.forEach((item) => { + cmd = `${cmd} ${item.id}` + }) + this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); childProcess.exec(cmd, execArgs, (err, stdout) => { @@ -99,8 +103,9 @@ export class MinitestTestRunner extends TestRunner { */ handleStatus(test: any, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { - context.passed(test.id) + context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { let errorMessageLine: number = test.line_number; let errorMessage: string = test.exception.message; @@ -122,7 +127,7 @@ export class MinitestTestRunner extends TestRunner { } context.failed( - test.id, + testItem, errorMessage, test.file_path.replace('./', ''), errorMessageLine - 1 @@ -130,7 +135,7 @@ export class MinitestTestRunner extends TestRunner { } else if (test.status === "failed" && test.pending_message !== null) { // Handle pending test cases. context.errored( - test.id, + testItem, test.pending_message, test.file_path.replace('./', ''), test.line_number diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index ba2c237..91b73c6 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -10,15 +10,14 @@ export class RspecTestRunner extends TestRunner { * * @return The raw output from the RSpec JSON formatter. */ - initTests = async () => new Promise((resolve, reject) => { + initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { let cfg = this.config as RspecConfig let cmd = `${cfg.getTestCommandWithFilePattern()} --require ${cfg.getCustomFormatterLocation()}` + ` --format CustomFormatter --order defined --dry-run`; - // TODO: Only reload single file on file changed - // if (testFilePath) { - // cmd = cmd + ` ${testFilePath}` - // } + testItems.forEach((item) => { + cmd = `${cmd} ${item.id}` + }) this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); this.log.debug(`cwd: ${__dirname}`) @@ -69,8 +68,9 @@ export class RspecTestRunner extends TestRunner { */ handleStatus(test: any, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { - context.passed(test.id) + context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { // Remove linebreaks from error message. let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); @@ -102,14 +102,14 @@ export class RspecTestRunner extends TestRunner { } context.failed( - test.id, + testItem, errorMessage, filePath, (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, ) } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { // Handle pending test cases. - context.skipped(test.id) + context.skipped(testItem) } }; diff --git a/src/testFactory.ts b/src/testFactory.ts index a0eaa11..ec6a8d3 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -6,12 +6,14 @@ import { Config } from './config'; import { TestLoader } from './testLoader'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; +import { TestSuite } from './testSuite'; export class TestFactory implements vscode.Disposable { private loader: TestLoader | null = null; private runner: RspecTestRunner | MinitestTestRunner | null = null; protected disposables: { dispose(): void }[] = []; protected framework: string; + private testSuite: TestSuite constructor( private readonly log: IVSCodeExtLogger, @@ -21,6 +23,7 @@ export class TestFactory implements vscode.Disposable { ) { this.disposables.push(this.configWatcher()); this.framework = Config.getTestFramework(this.log); + this.testSuite = new TestSuite(this.log, this.controller, this.config) } dispose(): void { @@ -37,13 +40,15 @@ export class TestFactory implements vscode.Disposable { this.log, this.workspace, this.controller, - this.config + this.config, + this.testSuite ) : new MinitestTestRunner( this.log, this.workspace, this.controller, - this.config + this.config, + this.testSuite ) this.disposables.push(this.runner); } @@ -57,7 +62,8 @@ export class TestFactory implements vscode.Disposable { this.workspace, this.controller, this.getRunner(), - this.config + this.config, + this.testSuite ) this.disposables.push(this.loader) } diff --git a/src/testLoader.ts b/src/testLoader.ts index 05e4e90..458646c 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,11 +1,11 @@ import * as vscode from 'vscode'; -import glob from 'glob'; import * as path from 'path'; import { IChildLogger } from '@vscode-logging/logger'; import { TestRunner } from './testRunner'; import { RspecTestRunner } from './rspec/rspecTestRunner'; import { MinitestTestRunner } from './minitest/minitestTestRunner'; import { Config } from './config'; +import { TestSuite } from './testSuite'; export type ParsedTest = { id: string, @@ -24,10 +24,10 @@ export class TestLoader implements vscode.Disposable { private readonly workspace: vscode.WorkspaceFolder | undefined, private readonly controller: vscode.TestController, private readonly testRunner: RspecTestRunner | MinitestTestRunner, - private readonly config: Config + private readonly config: Config, + private readonly testSuite: TestSuite, ) { this.log = rootLog.getChildLogger({label: "TestLoader"}); - this.disposables.push(this.createWatcher()); this.disposables.push(this.configWatcher()); } @@ -38,46 +38,41 @@ export class TestLoader implements vscode.Disposable { this.disposables = []; } - buildInitialTestItems() { - let log = this.log.getChildLogger({ label: `${this.buildInitialTestItems.name}` }) + async createWatcher(pattern: vscode.GlobPattern): Promise { + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + + // When files are created, make sure there's a corresponding "file" node in the tree + watcher.onDidCreate(uri => this.testSuite.getOrCreateTestItem(uri)); + // When files change, re-parse them. Note that you could optimize this so + // that you only re-parse children that have been resolved in the past. + watcher.onDidChange(uri => this.parseTestsInFile(uri)); + // And, finally, delete TestItems for removed files. This is simple, since + // we use the URI as the TestItem's ID. + watcher.onDidDelete(uri => this.testSuite.deleteTestItem(uri)); + + for (const file of await vscode.workspace.findFiles(pattern)) { + this.testSuite.getOrCreateTestItem(file); + } + + return watcher; + } + + discoverAllFilesInWorkspace() { + let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) let testDir = path.resolve(this.workspace?.uri.fsPath ?? '.', this.config.getTestDirectory()) - let testGlob = path.join('**', `+(${this.config.getFilePattern().join('|')})`) - log.info(`Looking for test files in ${testDir} using glob patterns`, this.config.getFilePattern()) - glob(testGlob, { cwd: testDir }, (err, files) => { - if (err) { - log.error("Error searching for test files", err); + let patterns: Array = [] + this.config.getFilePattern().forEach(pattern => { + if (vscode.workspace.workspaceFolders) { + vscode.workspace.workspaceFolders!.forEach(async (workspaceFolder) => { + patterns.push(new vscode.RelativePattern(workspaceFolder, pattern)) + }) } + patterns.push(new vscode.RelativePattern(testDir, pattern)) + }) - // Add files to the test suite - files.forEach(f => { - let fileSegments = f.split(path.sep) - let id:string = '' - let collection: vscode.TestItemCollection = this.controller.items - for (let i = 0; i < fileSegments.length; i++) { - id = path.join(id, fileSegments[i]) - let testItem: vscode.TestItem | undefined = collection.get(id) - if (testItem) { - collection = testItem.children - continue - } else { - let fPath = path.resolve(testDir, f) - - testItem = this.controller.createTestItem(id, fileSegments[i], vscode.Uri.file(fPath)) - if(fileSegments[i].includes('.')) { - // If a file and not a dir, allow to resolve the tests inside - log.debug(`Adding test item for file ${fPath}`) - testItem.canResolveChildren = true - } else { - log.debug(`Adding test item for folder ${fPath}`) - } - // testItem.busy = true Maybe? - collection.add(testItem) - collection = testItem.children - } - } - }); - }); + log.debug("Setting up watchers with the following test patterns", patterns) + return Promise.all(patterns.map(async (pattern) => await this.createWatcher(pattern))) } /** @@ -86,11 +81,11 @@ export class TestLoader implements vscode.Disposable { * * @return The full test suite. */ - public async loadAllTests(): Promise { - let log = this.log.getChildLogger({label:"loadAddTests"}) - log.info(`Loading Ruby tests (${this.config.frameworkName()})...`); + public async loadTests(testItem: vscode.TestItem): Promise { + let log = this.log.getChildLogger({label:"loadTests"}) + log.info(`Loading tests for ${testItem} (${this.config.frameworkName()})...`); try { - let output = await this.testRunner.initTests(); + let output = await this.testRunner.initTests([testItem]); log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); output = TestRunner.getJsonFromOutput(output); @@ -116,139 +111,14 @@ export class TestLoader implements vscode.Disposable { } ); - log.debug("Test output parsed. Building test suite", tests) - let testSuite: vscode.TestItem[] = await this.getBaseTestSuite(tests); - - // // Sort the children of each test suite based on their location in the test tree. - // testSuite.forEach((suite: vscode.TestItem) => { - // // NOTE: This will only sort correctly if everything is nested at the same - // // level, e.g. 111, 112, 121, etc. Once a fourth level of indentation is - // // introduced, the location is generated as e.g. 1231, which won't - // // sort properly relative to everything else. - // (suite.children as Array).sort((a: TestInfo, b: TestInfo) => { - // if ((a as TestInfo).type === "test" && (b as TestInfo).type === "test") { - // let aLocation: number = this.getTestLocation(a as TestInfo); - // let bLocation: number = this.getTestLocation(b as TestInfo); - // return aLocation - bLocation; - // } else { - // return 0; - // } - // }) - // }); - - this.controller.items.replace(testSuite); + log.debug("Test output parsed. Adding tests to test suite", tests) + await this.getTestSuiteForFile(tests, testItem); } catch (e: any) { log.error("Failed to load tests", e) return } } - /** - * Get the test directory based on the configuration value if there's a configured test framework. - */ - private getTestDirectory(): string | undefined { - let testDirectory = this.config.getTestDirectory(); - - if (testDirectory === '' || !this.workspace) { - return undefined; - } - - if (testDirectory.startsWith("./")) { - testDirectory = testDirectory.substring(2) - } - - return path.join(this.workspace.uri.fsPath, testDirectory); - } - - /** - * Create the base test suite with a root node and one layer of child nodes - * representing the subdirectories of spec/, and then any files under the - * given subdirectory. - * - * @param tests Test objects returned by our custom RSpec formatter or Minitest Rake task. - * @return The test suite root with its children. - */ - private async getBaseTestSuite(tests: ParsedTest[]): Promise { - let log = this.log.getChildLogger({ label: "getBaseTestSuite" }) - let testSuite: vscode.TestItem[] = [] - let testCount = 0 - let dirPath = this.getTestDirectory() ?? '.' - - // Create an array of all test files and then abuse Sets to make it unique. - let uniqueFiles = [...new Set(tests.map((test: { file_path: string; }) => test.file_path))]; - - let splitFilesArray: Array = []; - - log.debug("Building base test suite from files", uniqueFiles) - - // Remove the spec/ directory from all the file path. - uniqueFiles.forEach((file) => { - if (file.startsWith('.')) { - file = file.substring(1) - } - if (file.startsWith(path.sep)) { - file = file.substring(1) - } - splitFilesArray.push(file.split('/')); - }); - - // This gets the main types of tests, e.g. features, helpers, models, requests, etc. - let subdirectories: Array = []; - splitFilesArray.forEach((splitFile) => { - if (splitFile.length > 1) { - subdirectories.push(splitFile[0]); - } - }); - if (subdirectories[0] === ".") { - subdirectories = subdirectories.slice(1) - } - subdirectories = [...new Set(subdirectories)]; - log.debug("Found subdirectories:", subdirectories) - - // A nested loop to iterate through the direct subdirectories of spec/ and then - // organize the files under those subdirectories. - subdirectories.forEach((directory) => { - let subDirPath = path.join(dirPath, directory) - let uniqueFilesInDirectory: Array = uniqueFiles.filter((file) => { - let fullFilePath = path.resolve(dirPath, file) - log.debug(`Checking to see if ${fullFilePath} is in dir ${subDirPath}`) - return subDirPath === fullFilePath.substring(0, fullFilePath.lastIndexOf(path.sep)) - }); - log.debug(`Files in subdirectory (${directory}):`, uniqueFilesInDirectory) - - let directoryTestSuite: vscode.TestItem = this.controller.createTestItem(directory, directory, vscode.Uri.file(subDirPath)); - //directoryTestSuite.description = directory - - // Get the sets of tests for each file in the current directory. - uniqueFilesInDirectory.forEach((currentFile: string) => { - let currentFileTestSuite = this.getTestSuiteForFile(tests, currentFile, dirPath); - directoryTestSuite.children.add(currentFileTestSuite); - testCount += currentFileTestSuite.children.size + 1 - }); - - testSuite.push(directoryTestSuite); - testCount++ - }); - - // Sort test suite types alphabetically. - //testSuite = this.sortTestSuiteChildren(testSuite); - - // Get files that are direct descendants of the spec/ directory. - let topDirectoryFiles = uniqueFiles.filter((filePath) => { - return filePath.split('/').length === 1; - }); - log.debug(`Files in top directory:`, topDirectoryFiles) - - topDirectoryFiles.forEach((currentFile) => { - let currentFileTestSuite = this.getTestSuiteForFile(tests, currentFile, dirPath); - testSuite.push(currentFileTestSuite); - testCount += currentFileTestSuite.children.size + 1 - }); - - log.debug(`Returning ${testCount} test cases`) - return testSuite; - } - /** * Get the tests in a given file. * @@ -256,30 +126,17 @@ export class TestLoader implements vscode.Disposable { * @param currentFile Name of the file we're checking for tests * @param dirPath Full path to the root test folder */ - public getTestSuiteForFile(tests: Array, currentFile: string, dirPath: string): vscode.TestItem { - let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${currentFile})` }) + public getTestSuiteForFile(tests: Array, testItem: vscode.TestItem) { + let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${testItem.id})` }) let currentFileTests = tests.filter(test => { - return test.file_path === currentFile - }); + return test.file_path === testItem.uri?.fsPath + }) - let currentFileSplitName = currentFile.split(path.sep); - let currentFileLabel = currentFileSplitName[currentFileSplitName.length - 1] + let currentFileSplitName = testItem.uri?.fsPath.split(path.sep); + let currentFileLabel = currentFileSplitName ? currentFileSplitName[currentFileSplitName!.length - 1] : testItem.label let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - let testDir = this.getTestDirectory() - if (!testDir) { - log.fatal("No test folder configured or workspace folder open") - throw new Error("Missing test folders") - } - let currentFileAsAbsolutePath = path.resolve(dirPath, currentFile); - - let currentFileTestSuite: vscode.TestItem = this.controller.createTestItem( - currentFile, - currentFileLabel, - vscode.Uri.file(currentFileAsAbsolutePath) - ); - currentFileTests.forEach((test) => { log.debug(`Building test: ${test.id}`) // RSpec provides test ids like "file_name.rb[1:2:3]". @@ -307,15 +164,16 @@ export class TestLoader implements vscode.Disposable { description = description.replace(regex, ''); } - let testItem = this.controller.createTestItem(test.id, description, vscode.Uri.file(currentFileAsAbsolutePath)); - testItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); + let childTestItem = this.testSuite.getOrCreateTestItem(test.id) + childTestItem.label = description + childTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); - currentFileTestSuite.children.add(testItem); + testItem.children.add(childTestItem); }); - - return currentFileTestSuite; } + + /** * Convert a string from snake_case to PascalCase. * Note that the function will return the input string unchanged if it @@ -332,32 +190,28 @@ export class TestLoader implements vscode.Disposable { /** * Create a file watcher that will reload the test tree when a relevant file is changed. */ - private createWatcher(): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(document => { - if (!this.workspace) + public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { + let testItem: vscode.TestItem + if ("fsPath" in uri) { + let test = this.testSuite.getTestItem(uri) + if (!test) { return - - const filename = document.uri.fsPath; - this.log.info(`${filename} was saved - checking if this affects ${this.workspace.uri.fsPath}`); - if (filename.startsWith(this.workspace.uri.fsPath)) { - let testDirectory = this.getTestDirectory(); - - // In the case that there's no configured test directory, we shouldn't try to reload the tests. - if (testDirectory !== undefined && filename.startsWith(testDirectory)) { - this.log.info('A test file has been edited, reloading tests.'); - - // TODO: Reload only single file - this.loadAllTests(); - } } - }) + testItem = test + } else { + testItem = uri + } + + this.log.info('A test file has been edited, reloading tests.'); + await this.loadTests(testItem) } private configWatcher(): vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer")) { - this.loadAllTests(); + this.controller.items.replace([]) + this.discoverAllFilesInWorkspace(); } }) } diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 9aaef9b..08d93ee 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode' -import path from 'path' import { IChildLogger } from '@vscode-logging/logger' import { Config } from './config' @@ -23,8 +22,8 @@ export class TestRunContext { constructor( public readonly log: IChildLogger, public readonly token: vscode.CancellationToken, - request: vscode.TestRunRequest, - private readonly controller: vscode.TestController, + readonly request: vscode.TestRunRequest, + readonly controller: vscode.TestController, public readonly config: Config, public readonly debuggerConfig?: vscode.DebugConfiguration ) { @@ -36,19 +35,9 @@ export class TestRunContext { * * @param testId ID of the test item to update. */ - public enqueued(test: string | vscode.TestItem): void { - if (typeof test === "string") { - try { - this.testRun.enqueued(this.getTestItem(test)) - this.log.debug(`Enqueued: ${test}`) - } catch (e: any) { - this.log.error(`Failed to set test ${test} as Enqueued`, e) - } - } - else { - this.testRun.enqueued(test) - this.log.debug(`Enqueued: ${test.id}`) - } + public enqueued(test: vscode.TestItem): void { + this.testRun.enqueued(test) + this.log.debug(`Enqueued: ${test.id}`) } /** @@ -61,7 +50,7 @@ export class TestRunContext { * @param duration How long the test took to execute, in milliseconds. */ public errored( - testId: string, + test: vscode.TestItem, message: string, file: string, line: number, @@ -69,13 +58,13 @@ export class TestRunContext { ): void { let testMessage = new vscode.TestMessage(message) try { - let testItem = this.getTestItem(testId) + let testItem = test testMessage.location = new vscode.Location( testItem.uri ?? vscode.Uri.file(file), new vscode.Position(line, 0) ) this.testRun.errored(testItem, testMessage, duration) - this.log.debug(`Errored: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) + this.log.debug(`Errored: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) } catch (e: any) { this.log.error(`Failed to set test ${test} as Errored`, e) } @@ -91,24 +80,19 @@ export class TestRunContext { * @param duration How long the test took to execute, in milliseconds. */ public failed( - testId: string, + test: vscode.TestItem, message: string, file: string, line: number, duration?: number | undefined ): void { - try { - let testMessage = new vscode.TestMessage(message) - let testItem = this.getTestItem(testId) - testMessage.location = new vscode.Location( - testItem.uri ?? vscode.Uri.file(file), - new vscode.Position(line, 0) - ) - this.testRun.failed(testItem, testMessage, duration) - this.log.debug(`Failed: ${testId} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) - } catch (e: any) { - this.log.error(`Failed to set test ${test} as Failed`, e) - } + let testMessage = new vscode.TestMessage(message) + testMessage.location = new vscode.Location( + test.uri ?? vscode.Uri.file(file), + new vscode.Position(line, 0) + ) + this.testRun.failed(test, testMessage, duration) + this.log.debug(`Failed: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) } /** @@ -117,16 +101,11 @@ export class TestRunContext { * @param testId ID of the test item to update. * @param duration How long the test took to execute, in milliseconds. */ - public passed( - testId: string, + public passed(test: vscode.TestItem, duration?: number | undefined ): void { - try { - this.testRun.passed(this.getTestItem(testId), duration) - this.log.debug(`Passed: ${testId}${duration ? `, duration: ${duration}ms` : ''}`) - } catch (e: any) { - this.log.error(`Failed to set test ${test} as Passed`, e) - } + this.testRun.passed(test, duration) + this.log.debug(`Passed: ${test.id}${duration ? `, duration: ${duration}ms` : ''}`) } /** @@ -134,19 +113,9 @@ export class TestRunContext { * * @param test ID of the test item to update, or the test item. */ - public skipped(test: string | vscode.TestItem): void { - if (typeof test === "string") { - try { - this.testRun.skipped(this.getTestItem(test)) - this.log.debug(`Skipped: ${test}`) - } catch (e: any) { - this.log.error(`Failed to set test ${test} as Skipped`, e) - } - } - else { - this.testRun.skipped(test) - this.log.debug(`Skipped: ${test.id}`) - } + public skipped(test: vscode.TestItem): void { + this.testRun.skipped(test) + this.log.debug(`Skipped: ${test.id}`) } /** @@ -154,60 +123,8 @@ export class TestRunContext { * * @param testId ID of the test item to update, or the test item. */ - public started(test: string | vscode.TestItem): void { - if (typeof test === "string") { - try { - this.testRun.started(this.getTestItem(test)) - this.log.debug(`Started: ${test}`) - } catch (e: any) { - this.log.error(`Failed to set test ${test} as Started`, e) - } - } - else { - this.testRun.started(test) - this.log.debug(`Started: ${test.id}`) - } - } - - /** - * Get the {@link vscode.TestItem} for a test ID - * @param testId Test ID to lookup - * @returns The test item for the ID - * @throws if test item could not be found - */ - public getTestItem(testId: string): vscode.TestItem { - let log = this.log.getChildLogger({label: `${this.getTestItem.name}(${testId})`}) - testId = testId.replace(/^\.\/spec\//, '') - let idSegments = testId.split(path.sep) - let collection: vscode.TestItemCollection = this.controller.items - - // Walk the test hierarchy to find the collection containing our test file - for (let i = 0; i < idSegments.length - 1; i++) { - let collectionId = (i == 0) - ? idSegments[0] - : idSegments.slice(0,i).join(path.sep) - let childCollection = collection.get(collectionId)?.children - if (!childCollection) { - throw `Test collection not found: ${collectionId}` - } - collection = childCollection - } - - // Need to make sure we strip locations from file id to get final collection - let fileId = testId.replace(/\[[0-9](?::[0-9])*\]$/, '') - let childCollection = collection.get(fileId)?.children - if (!childCollection) { - throw `Test collection not found: ${fileId}` - } - collection = childCollection - log.debug("Got parent collection, looking for test") - - let testItem = collection.get(testId) - if (!testItem) { - // Create a basic test item with what little info we have to be filled in later - testItem = this.controller.createTestItem(testId, idSegments[idSegments.length - 1], vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId))); - collection.add(testItem); - } - return testItem + public started(test: vscode.TestItem): void { + this.testRun.started(test) + this.log.debug(`Started: ${test.id}`) } } diff --git a/src/testRunner.ts b/src/testRunner.ts index 1624c95..563955d 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -6,10 +6,10 @@ import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; +import { TestSuite } from './testSuite'; export abstract class TestRunner implements vscode.Disposable { protected currentChildProcess: childProcess.ChildProcess | undefined; - protected testSuite: vscode.TestItem[] | undefined; protected debugCommandStartedResolver: Function | undefined; protected disposables: { dispose(): void }[] = []; protected readonly log: IChildLogger; @@ -23,7 +23,8 @@ export abstract class TestRunner implements vscode.Disposable { rootLog: IChildLogger, protected workspace: vscode.WorkspaceFolder | undefined, protected controller: vscode.TestController, - protected config: RspecConfig | MinitestConfig + protected config: RspecConfig | MinitestConfig, + protected testSuite: TestSuite, ) { this.log = rootLog.getChildLogger({label: "TestRunner"}) } @@ -32,7 +33,7 @@ export abstract class TestRunner implements vscode.Disposable { * Initialise the test framework, parse tests (without executing) and retrieve the output * @return Stdout outpu from framework initialisation */ - abstract initTests: () => Promise; + abstract initTests: (testItems: vscode.TestItem[]) => Promise; public dispose() { this.killChild(); @@ -134,16 +135,20 @@ export abstract class TestRunner implements vscode.Disposable { childProcessLogger.debug(data); if (data.startsWith('PASSED:')) { data = data.replace('PASSED: ', ''); - context.passed(data) + let test = this.testSuite.getOrCreateTestItem(data) + context.passed(test) } else if (data.startsWith('FAILED:')) { data = data.replace('FAILED: ', ''); - context.failed(data, "", "", 0) + let test = this.testSuite.getOrCreateTestItem(data) + context.failed(test, "", "", 0) } else if (data.startsWith('RUNNING:')) { data = data.replace('RUNNING: ', ''); - context.started(data) + let test = this.testSuite.getOrCreateTestItem(data) + context.started(test) } else if (data.startsWith('PENDING:')) { data = data.replace('PENDING: ', ''); - context.enqueued(data) + let test = this.testSuite.getOrCreateTestItem(data) + context.enqueued(test) } if (data.includes('START_OF_TEST_JSON')) { resolve(data); diff --git a/src/testSuite.ts b/src/testSuite.ts new file mode 100644 index 0000000..c63f504 --- /dev/null +++ b/src/testSuite.ts @@ -0,0 +1,92 @@ +import * as vscode from 'vscode' +import path from 'path' +import { IChildLogger } from '@vscode-logging/logger'; +import { Config } from './config'; + +export class TestSuite { + private readonly log: IChildLogger; + + constructor( + readonly rootLog: IChildLogger, + private readonly controller: vscode.TestController, + private readonly config: Config + ) { + this.log = rootLog.getChildLogger({label: "TestSuite"}); + } + + public deleteTestItem(testId: string | vscode.Uri) { + let log = this.log.getChildLogger({label: 'deleteTestItem'}) + testId = this.uriToTestId(testId) + let collection = this.getParentTestItemCollection(testId) + let testItem = collection.get(testId) + if (testItem) { + collection.delete(testItem.id); + log.debug(`Removed test ${testItem.id}`) + } + } + + /** + * Get the {@link vscode.TestItem} for a test ID + * @param testId Test ID to lookup + * @returns The test item for the ID + * @throws if test item could not be found + */ + public getOrCreateTestItem(testId: string | vscode.Uri): vscode.TestItem { + let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) + testId = this.uriToTestId(testId) + let collection = this.getParentTestItemCollection(testId) + let testItem = collection.get(testId) + if (!testItem) { + // Create a basic test item with what little info we have to be filled in later + testItem = this.controller.createTestItem(testId, testId.substring(testId.lastIndexOf(path.sep)), vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId))); + collection.add(testItem); + log.debug(`Added test ${testItem.id}`) + } + return testItem + } + + public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { + let log = this.log.getChildLogger({label: 'getTestItem'}) + testId = this.uriToTestId(testId) + let collection = this.getParentTestItemCollection(testId) + let testItem = collection.get(testId) + if (!testItem) { + log.debug(`Couldn't find ${testId}`) + return undefined + } + return testItem + } + + private uriToTestId(uri: string | vscode.Uri): string { + if (typeof uri === "string") + return uri + return uri.fsPath.replace(path.resolve(this.config.getTestDirectory()), '') + } + + private getParentTestItemCollection(testId: string): vscode.TestItemCollection { + testId = testId.replace(/^\.\/spec\//, '') + let idSegments = testId.split(path.sep) + let collection: vscode.TestItemCollection = this.controller.items + + // Walk the test hierarchy to find the collection containing our test file + for (let i = 0; i < idSegments.length - 1; i++) { + let collectionId = (i == 0) + ? idSegments[0] + : idSegments.slice(0,i).join(path.sep) + let childCollection = collection.get(collectionId)?.children + if (!childCollection) { + throw `Test collection not found: ${collectionId}` + } + collection = childCollection + } + + // Need to make sure we strip locations from file id to get final collection + let fileId = testId.replace(/\[[0-9](?::[0-9])*\]$/, '') + let childCollection = collection.get(fileId)?.children + if (!childCollection) { + throw `Test collection not found: ${fileId}` + } + collection = childCollection + return collection + } +} \ No newline at end of file diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 0e1d5ce..368d034 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -7,6 +7,7 @@ import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { StubTestController } from '../../stubs/stubTestController'; import { expect } from 'chai'; +import { TestSuite } from 'src/testSuite'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -29,13 +30,111 @@ suite('Extension Test for RSpec', function() { this.beforeEach(async function() { testController = new StubTestController() + + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string) => testController.createTestItem(id, id, vscode.Uri.file(expectedPath(id))) + testController.items.add(createTest("abs_spec.rb")) + testController.items.add(createTest("square_spec.rb")) + let subfolderItem = createTest("subfolder") + testController.items.add(subfolderItem) + subfolderItem.children.add(createTest("subfolder/foo_spec.rb")) + config = new RspecConfig(dirPath) - testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config) - testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config); + let testSuite = new TestSuite(stdout_logger(), testController, config) + testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config, testSuite); + }) + + test('Load tests on file resolve request', async function () { + // No tests in suite initially, only test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [] + } + ] + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + children: [] + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb", + label: "square_spec.rb", + children: [] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [] + } + ] + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + ] + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb", + label: "square_spec.rb", + children: [] + }, + ] + ) }) test('Load all tests', async function() { - await testLoader.loadAllTests() + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("subfolder/foo_spec.rb"))) const testSuite = testController.items @@ -112,7 +211,7 @@ suite('Extension Test for RSpec', function() { }) test('run test success', async function() { - await testLoader.loadAllTests() + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) let mockRequest = setupMockRequest(testController, "square_spec.rb") let request = instance(mockRequest) @@ -136,7 +235,7 @@ suite('Extension Test for RSpec', function() { }) test('run test failure', async function() { - await testLoader.loadAllTests() + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) let mockRequest = setupMockRequest(testController, "square_spec.rb") let request = instance(mockRequest) @@ -174,7 +273,7 @@ suite('Extension Test for RSpec', function() { }) test('run test error', async function() { - await testLoader.loadAllTests() + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:2]") let request = instance(mockRequest) @@ -211,7 +310,7 @@ suite('Extension Test for RSpec', function() { }) test('run test skip', async function() { - await testLoader.loadAllTests() + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:3]") let request = instance(mockRequest) diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 6a6bf4a..94b94f0 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -1,230 +1,222 @@ -import { setupMockTestController, stdout_logger, testItemArrayMatches } from "../helpers"; -import { instance, mock, spy, when } from 'ts-mockito' -import * as vscode from 'vscode' -import * as path from 'path' -import { expect } from "chai"; -import { ParsedTest, TestLoader } from "../../../src/testLoader"; -import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; -import { RspecConfig } from "../../../src/rspec/rspecConfig"; - -suite('TestLoader', function() { - let mockTestController = setupMockTestController() - let mockTestRunner = mock() - let loader:TestLoader - - const rspecDir = path.resolve("./test/fixtures/rspec/spec") - const config = new RspecConfig(path.resolve("./ruby")) - const configWrapper: RspecConfig = { - frameworkName: config.frameworkName, - getTestCommand: config.getTestCommand, - getDebugCommand: config.getDebugCommand, - getTestCommandWithFilePattern: config.getTestCommandWithFilePattern, - getTestDirectory: () => rspecDir, - getCustomFormatterLocation: config.getCustomFormatterLocation, - testCommandWithFormatterAndDebugger: config.testCommandWithFormatterAndDebugger, - getProcessEnv: config.getProcessEnv, - rubyScriptPath: config.rubyScriptPath, - getFilePattern: config.getFilePattern - } - - suite('#getBaseTestSuite()', function() { - this.beforeEach(function() { - let spiedWorkspace = spy(vscode.workspace) - when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) - .thenReturn({ get: (section: string) => { - section == "framework" ? "rspec" : undefined - }} as vscode.WorkspaceConfiguration) - - - loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], instance(mockTestController), instance(mockTestRunner), configWrapper) - let loaderSpy = spy(loader) - when(loaderSpy["getTestDirectory"]()).thenReturn(rspecDir) - }) - - this.afterEach(function() { - loader.dispose() - }) - - test('no input, no output, no error', async function() { - let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"]([] as ParsedTest[])).to.not.throw - - expect(testItems).to.not.be.undefined - expect(testItems).to.have.length(0) - }); - - test('single file with one test case', async function() { - let tests: ParsedTest[] = [ - { - id: "abs_spec.rb[1:1]", - full_description: "Abs finds the absolute value of 1", - description: "finds the absolute value of 1", - file_path: "abs_spec.rb", - line_number: 4, - location: 11, - } - ]; - let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - - expect(testItems).to.not.be.undefined - expect(testItems).to.have.length(1) - testItemArrayMatches(testItems, [ - { - id: "abs_spec.rb", - file: path.join(rspecDir, "abs_spec.rb"), - label: "abs_spec.rb", - children: [ - { - id: "abs_spec.rb[1:1]", - file: path.join(rspecDir, "abs_spec.rb"), - label: "finds the absolute value of 1", - line: 3 - } - ] - } - ]) - }) - - test('single file with two test cases', async function() { - let tests: ParsedTest[] = [ - { - id: "abs_spec.rb[1:1]", - full_description: "Abs finds the absolute value of 1", - description: "finds the absolute value of 1", - file_path: "abs_spec.rb", - line_number: 4, - location: 11, - }, - { - id: "abs_spec.rb[1:2]", - full_description: "Abs finds the absolute value of 0", - description: "finds the absolute value of 0", - file_path: "abs_spec.rb", - line_number: 8, - location: 12, - } - ]; - let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - - expect(testItems).to.not.be.undefined - expect(testItems).to.have.length(1) - testItemArrayMatches(testItems, [ - { - id: "abs_spec.rb", - file: path.join(rspecDir, "abs_spec.rb"), - label: "abs_spec.rb", - children: [ - { - id: "abs_spec.rb[1:1]", - file: path.join(rspecDir, "abs_spec.rb"), - label: "finds the absolute value of 1", - line: 3 - }, - { - id: "abs_spec.rb[1:2]", - file: path.join(rspecDir, "abs_spec.rb"), - label: "finds the absolute value of 0", - line: 7 - }, - ] - } - ]) - }) - - test('two files, one with a suite, each with one test case', async function() { - let tests: ParsedTest[] = [ - { - id: "abs_spec.rb[1:1]", - full_description: "Abs finds the absolute value of 1", - description: "finds the absolute value of 1", - file_path: "abs_spec.rb", - line_number: 4, - location: 11, - }, - { - id: "square_spec.rb[1:1:1]", - full_description: "Square an unnecessary suite finds the square of 2", - description: "finds the square of 2", - file_path: "square_spec.rb", - line_number: 5, - location: 111, - } - ]; - let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - - expect(testItems).to.not.be.undefined - expect(testItems).to.have.length(2) - testItemArrayMatches(testItems, [ - { - id: "abs_spec.rb", - file: path.join(rspecDir, "abs_spec.rb"), - label: "abs_spec.rb", - children: [ - { - id: "abs_spec.rb[1:1]", - file: path.join(rspecDir, "abs_spec.rb"), - label: "finds the absolute value of 1", - line: 3 - } - ] - }, - { - id: "square_spec.rb", - file: path.join(rspecDir, "square_spec.rb"), - label: "square_spec.rb", - children: [ - { - id: "square_spec.rb[1:1:1]", - file: path.join(rspecDir, "square_spec.rb"), - label: "an unnecessary suite finds the square of 2", - line: 4 - }, - ] - } - ]) - }) - - test('subfolder containing single file with one test case', async function() { - let tests: ParsedTest[] = [ - { - id: "subfolder/foo_spec.rb[1:1]", - full_description: "Foo wibbles and wobbles", - description: "wibbles and wobbles", - file_path: "subfolder/foo_spec.rb", - line_number: 3, - location: 11, - } - ]; - let testItems: vscode.TestItem[] - expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - - expect(testItems).to.not.be.undefined - expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') - testItemArrayMatches(testItems, [ - { - id: "subfolder", - file: path.join(rspecDir, "subfolder"), - label: "subfolder", - children: [ - { - id: "subfolder/foo_spec.rb", - file: path.join(rspecDir, "subfolder", "foo_spec.rb"), - label: "foo_spec.rb", - children: [ - { - id: "subfolder/foo_spec.rb[1:1]", - file: path.join(rspecDir, "subfolder", "foo_spec.rb"), - label: "wibbles and wobbles", - line: 2 - } - ] - } - ] - } - ]) - }) - }) -}) \ No newline at end of file +// import { setupMockTestController, stdout_logger, testItemArrayMatches, testItemCollectionMatches } from "../helpers"; +// import { instance, mock, spy, when } from 'ts-mockito' +// import * as vscode from 'vscode' +// import * as path from 'path' +// import { expect } from "chai"; +// import { ParsedTest, TestLoader } from "../../../src/testLoader"; +// import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; +// import { RspecConfig } from "../../../src/rspec/rspecConfig"; +// import { TestSuite } from "../../../src/testSuite"; + +// suite('TestLoader', function() { +// let mockTestController = setupMockTestController() +// let mockTestRunner = mock() +// let loader:TestLoader +// let testSuite: TestSuite +// let testController: vscode.TestController = instance(mockTestController) + +// const rspecDir = path.resolve("./test/fixtures/rspec/spec") +// const config = new RspecConfig(path.resolve("./ruby")) +// const configWrapper: RspecConfig = { +// frameworkName: config.frameworkName, +// getTestCommand: config.getTestCommand, +// getDebugCommand: config.getDebugCommand, +// getTestCommandWithFilePattern: config.getTestCommandWithFilePattern, +// getTestDirectory: () => rspecDir, +// getCustomFormatterLocation: config.getCustomFormatterLocation, +// testCommandWithFormatterAndDebugger: config.testCommandWithFormatterAndDebugger, +// getProcessEnv: config.getProcessEnv, +// rubyScriptPath: config.rubyScriptPath, +// getFilePattern: config.getFilePattern +// } + +// suite('#getBaseTestSuite()', function() { +// this.beforeEach(function() { +// let spiedWorkspace = spy(vscode.workspace) +// when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) +// .thenReturn({ get: (section: string) => { +// section == "framework" ? "rspec" : undefined +// }} as vscode.WorkspaceConfiguration) + +// testSuite = new TestSuite(stdout_logger(), testController, config) +// loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], testController, instance(mockTestRunner), configWrapper, testSuite) +// let loaderSpy = spy(loader) +// }) + +// this.afterEach(function() { +// loader.dispose() +// }) + +// test('single file with one test case', async function() { +// let tests: ParsedTest[] = [ +// { +// id: "abs_spec.rb[1:1]", +// full_description: "Abs finds the absolute value of 1", +// description: "finds the absolute value of 1", +// file_path: "abs_spec.rb", +// line_number: 4, +// location: 11, +// } +// ]; +// await loader.parseTestsInFile(vscode.Uri.file(path.resolve(rspecDir, "abs_spec.rb"))) + +// expect(testController.items).to.have.property('size', 1) +// testItemCollectionMatches(testController.items, [ +// { +// id: "abs_spec.rb", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "abs_spec.rb", +// children: [ +// { +// id: "abs_spec.rb[1:1]", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "finds the absolute value of 1", +// line: 3 +// } +// ] +// } +// ]) +// }) + +// test('single file with two test cases', async function() { +// let tests: ParsedTest[] = [ +// { +// id: "abs_spec.rb[1:1]", +// full_description: "Abs finds the absolute value of 1", +// description: "finds the absolute value of 1", +// file_path: "abs_spec.rb", +// line_number: 4, +// location: 11, +// }, +// { +// id: "abs_spec.rb[1:2]", +// full_description: "Abs finds the absolute value of 0", +// description: "finds the absolute value of 0", +// file_path: "abs_spec.rb", +// line_number: 8, +// location: 12, +// } +// ]; +// let testItems: vscode.TestItem[] +// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw + +// expect(testItems).to.not.be.undefined +// expect(testItems).to.have.length(1) +// testItemArrayMatches(testItems, [ +// { +// id: "abs_spec.rb", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "abs_spec.rb", +// children: [ +// { +// id: "abs_spec.rb[1:1]", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "finds the absolute value of 1", +// line: 3 +// }, +// { +// id: "abs_spec.rb[1:2]", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "finds the absolute value of 0", +// line: 7 +// }, +// ] +// } +// ]) +// }) + +// test('two files, one with a suite, each with one test case', async function() { +// let tests: ParsedTest[] = [ +// { +// id: "abs_spec.rb[1:1]", +// full_description: "Abs finds the absolute value of 1", +// description: "finds the absolute value of 1", +// file_path: "abs_spec.rb", +// line_number: 4, +// location: 11, +// }, +// { +// id: "square_spec.rb[1:1:1]", +// full_description: "Square an unnecessary suite finds the square of 2", +// description: "finds the square of 2", +// file_path: "square_spec.rb", +// line_number: 5, +// location: 111, +// } +// ]; +// let testItems: vscode.TestItem[] +// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw + +// expect(testItems).to.not.be.undefined +// expect(testItems).to.have.length(2) +// testItemArrayMatches(testItems, [ +// { +// id: "abs_spec.rb", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "abs_spec.rb", +// children: [ +// { +// id: "abs_spec.rb[1:1]", +// file: path.join(rspecDir, "abs_spec.rb"), +// label: "finds the absolute value of 1", +// line: 3 +// } +// ] +// }, +// { +// id: "square_spec.rb", +// file: path.join(rspecDir, "square_spec.rb"), +// label: "square_spec.rb", +// children: [ +// { +// id: "square_spec.rb[1:1:1]", +// file: path.join(rspecDir, "square_spec.rb"), +// label: "an unnecessary suite finds the square of 2", +// line: 4 +// }, +// ] +// } +// ]) +// }) + +// test('subfolder containing single file with one test case', async function() { +// let tests: ParsedTest[] = [ +// { +// id: "subfolder/foo_spec.rb[1:1]", +// full_description: "Foo wibbles and wobbles", +// description: "wibbles and wobbles", +// file_path: "subfolder/foo_spec.rb", +// line_number: 3, +// location: 11, +// } +// ]; +// let testItems: vscode.TestItem[] +// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw + +// expect(testItems).to.not.be.undefined +// expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') +// testItemArrayMatches(testItems, [ +// { +// id: "subfolder", +// file: path.join(rspecDir, "subfolder"), +// label: "subfolder", +// children: [ +// { +// id: "subfolder/foo_spec.rb", +// file: path.join(rspecDir, "subfolder", "foo_spec.rb"), +// label: "foo_spec.rb", +// children: [ +// { +// id: "subfolder/foo_spec.rb[1:1]", +// file: path.join(rspecDir, "subfolder", "foo_spec.rb"), +// label: "wibbles and wobbles", +// line: 2 +// } +// ] +// } +// ] +// } +// ]) +// }) +// }) +// }) \ No newline at end of file From 9f806b77adf3fe47e90ec9c698195113c2dd6e8a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 2 Mar 2022 10:32:38 +0000 Subject: [PATCH 025/108] Get everything sort of working in a quick and dirty way For some reason I can't revert to the released version so I need to be able to run tests again on my work laptop >_< --- src/main.ts | 10 ++- src/minitest/minitestTestRunner.ts | 10 +-- src/rspec/rspecConfig.ts | 4 +- src/rspec/rspecTestRunner.ts | 65 ++++++++-------- src/testLoader.ts | 47 +++++++----- src/testRunner.ts | 115 +++++++++++++++-------------- src/testSuite.ts | 99 +++++++++++++++++++++++-- 7 files changed, 235 insertions(+), 115 deletions(-) diff --git a/src/main.ts b/src/main.ts index bb0f465..df8312d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,6 +62,11 @@ export async function activate(context: vscode.ExtensionContext) { const testLoaderFactory = new TestFactory(log, workspace, controller, testConfig); context.subscriptions.push(controller); + // TODO: REMOVE THIS! + // Temporary kludge to clear out stale items that have bad data during development + // Later on will add context menus to delete test items or force a refresh of everything + controller.items.replace([]) + testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); // TODO: Allow lazy-loading of child tests - below is taken from example in docs @@ -70,11 +75,10 @@ export async function activate(context: vscode.ExtensionContext) { // the test whose children VS Code wanted to load. controller.resolveHandler = async test => { log.debug('resolveHandler called', test) - // TODO: Glob child files and folders regardless of canResolveChildren - // TODO: If canResolveChildren, dry run test and update TestItems with response if (!test) { await testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); - } else { + } else if (test.id.endsWith(".rb")) { + // Only parse files await testLoaderFactory.getLoader().parseTestsInFile(test); } }; diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 6915b70..ad0f47f 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -80,14 +80,14 @@ export class MinitestTestRunner extends TestRunner { return cmd; } - protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { - let line = testLocation.split(':').pop(); - let relativeLocation = testLocation.split(/:\d+$/)[0].replace(`${this.workspace?.uri.fsPath || "."}/`, "") + protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { + let line = testItem.id.split(':').pop(); + let relativeLocation = testItem.id.split(/:\d+$/)[0].replace(`${this.workspace?.uri.fsPath || "."}/`, "") return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeLocation}:${line}'` }; - protected getTestFileCommand(testFile: string, context: TestRunContext): string { - let relativeFile = testFile.replace(`${this.workspace?.uri.fsPath || '.'}/`, "").replace(`./`, "") + protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { + let relativeFile = testItem.id.replace(`${this.workspace?.uri.fsPath || '.'}/`, "").replace(`./`, "") return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeFile}'` }; diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 0fc61b0..222907f 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -82,6 +82,8 @@ export class RspecConfig extends Config { public getTestDirectory(): string { let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string - return configDir ?? `.${path.sep}spec`; + if (configDir.startsWith('./')) + configDir = configDir.substring(2) + return configDir ?? `spec`; } } \ No newline at end of file diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 91b73c6..f4b844e 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,8 +1,10 @@ import * as vscode from 'vscode'; +import * as path from 'path' import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; import { RspecConfig } from './rspecConfig'; +import { ParsedTest } from 'src/testLoader'; export class RspecTestRunner extends TestRunner { /** @@ -12,11 +14,11 @@ export class RspecTestRunner extends TestRunner { */ initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { let cfg = this.config as RspecConfig - let cmd = `${cfg.getTestCommandWithFilePattern()} --require ${cfg.getCustomFormatterLocation()}` - + ` --format CustomFormatter --order defined --dry-run`; + let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; testItems.forEach((item) => { - cmd = `${cmd} ${item.id}` + let testPath = `${cfg.getTestDirectory()}${path.sep}${item.id}` + cmd = `${cmd} ${testPath}` }) this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); @@ -31,30 +33,34 @@ export class RspecTestRunner extends TestRunner { childProcess.exec(cmd, execArgs, (err, stdout) => { if (err) { - this.log.error(`Error while finding RSpec test suite: ${err.message}`); - // Show an error message. - vscode.window.showWarningMessage( - "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", - "View error message" - ).then(selection => { - if (selection === "View error message") { - let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); - let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); + if (err.message.includes('deprecated')) { + this.log.warn(`Warning while finding RSpec test suite: ${err.message}`) + } else { + this.log.error(`Error while finding RSpec test suite: ${err.message}`); + // Show an error message. + vscode.window.showWarningMessage( + "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", + "View error message" + ).then(selection => { + if (selection === "View error message") { + let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); + let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); - if (outputJson.messages.length > 0) { - let outputJsonString = outputJson.messages.join("\n\n"); - let outputJsonArray = outputJsonString.split("\n"); - outputJsonArray.forEach((line: string) => { - outputChannel.appendLine(line); - }) - } else { - outputChannel.append(err.message); + if (outputJson.messages.length > 0) { + let outputJsonString = outputJson.messages.join("\n\n"); + let outputJsonArray = outputJsonString.split("\n"); + outputJsonArray.forEach((line: string) => { + outputChannel.appendLine(line); + }) + } else { + outputChannel.append(err.message); + } + outputChannel.show(false); } - outputChannel.show(false); - } - }); + }); - throw err; + throw err; + } } resolve(stdout); }); @@ -66,10 +72,11 @@ export class RspecTestRunner extends TestRunner { * @param test The test that we want to handle. * @param context Test run context */ - handleStatus(test: any, context: TestRunContext): void { + handleStatus(test: ParsedTest, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { + // TODO: Parse additional test data context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { // Remove linebreaks from error message. @@ -113,12 +120,12 @@ export class RspecTestRunner extends TestRunner { } }; - protected getSingleTestCommand(testLocation: string, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testLocation}'` + protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getTestDirectory()}${path.sep}${testItem.id}'` }; - protected getTestFileCommand(testFile: string, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${testFile}'` + protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getTestDirectory()}${path.sep}${testItem.id}'` }; protected getFullTestSuiteCommand(context: TestRunContext): string { diff --git a/src/testLoader.ts b/src/testLoader.ts index 458646c..1760943 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -13,7 +13,10 @@ export type ParsedTest = { description: string, file_path: string, line_number: number, - location: number + location: number, + status?: string, + pending_message?: string, + exception?: any, } export class TestLoader implements vscode.Disposable { protected disposables: { dispose(): void }[] = []; @@ -39,16 +42,26 @@ export class TestLoader implements vscode.Disposable { } async createWatcher(pattern: vscode.GlobPattern): Promise { + let log = this.log.getChildLogger({label: `createWatcher(${pattern})`}) const watcher = vscode.workspace.createFileSystemWatcher(pattern); // When files are created, make sure there's a corresponding "file" node in the tree - watcher.onDidCreate(uri => this.testSuite.getOrCreateTestItem(uri)); + watcher.onDidCreate(uri => { + log.debug(`onDidCreate ${uri.fsPath}`) + this.testSuite.getOrCreateTestItem(uri) + }) // When files change, re-parse them. Note that you could optimize this so // that you only re-parse children that have been resolved in the past. - watcher.onDidChange(uri => this.parseTestsInFile(uri)); + watcher.onDidChange(uri => { + log.debug(`onDidChange ${uri.fsPath}`) + this.parseTestsInFile(uri) + }); // And, finally, delete TestItems for removed files. This is simple, since // we use the URI as the TestItem's ID. - watcher.onDidDelete(uri => this.testSuite.deleteTestItem(uri)); + watcher.onDidDelete(uri => { + log.debug(`onDidDelete ${uri.fsPath}`) + this.testSuite.deleteTestItem(uri) + }); for (const file of await vscode.workspace.findFiles(pattern)) { this.testSuite.getOrCreateTestItem(file); @@ -63,12 +76,13 @@ export class TestLoader implements vscode.Disposable { let patterns: Array = [] this.config.getFilePattern().forEach(pattern => { - if (vscode.workspace.workspaceFolders) { - vscode.workspace.workspaceFolders!.forEach(async (workspaceFolder) => { - patterns.push(new vscode.RelativePattern(workspaceFolder, pattern)) - }) - } - patterns.push(new vscode.RelativePattern(testDir, pattern)) + // TODO: Search all workspace folders (needs ability to exclude folders) + // if (vscode.workspace.workspaceFolders) { + // vscode.workspace.workspaceFolders!.forEach(async (workspaceFolder) => { + // patterns.push(new vscode.RelativePattern(workspaceFolder, '**/' + pattern)) + // }) + // } + patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) }) log.debug("Setting up watchers with the following test patterns", patterns) @@ -83,7 +97,7 @@ export class TestLoader implements vscode.Disposable { */ public async loadTests(testItem: vscode.TestItem): Promise { let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItem} (${this.config.frameworkName()})...`); + log.info(`Loading tests for ${testItem.id} (${this.config.frameworkName()})...`); try { let output = await this.testRunner.initTests([testItem]); @@ -115,7 +129,7 @@ export class TestLoader implements vscode.Disposable { await this.getTestSuiteForFile(tests, testItem); } catch (e: any) { log.error("Failed to load tests", e) - return + return Promise.reject(e) } } @@ -128,16 +142,13 @@ export class TestLoader implements vscode.Disposable { */ public getTestSuiteForFile(tests: Array, testItem: vscode.TestItem) { let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${testItem.id})` }) - let currentFileTests = tests.filter(test => { - return test.file_path === testItem.uri?.fsPath - }) let currentFileSplitName = testItem.uri?.fsPath.split(path.sep); let currentFileLabel = currentFileSplitName ? currentFileSplitName[currentFileSplitName!.length - 1] : testItem.label let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - currentFileTests.forEach((test) => { + tests.forEach((test) => { log.debug(`Building test: ${test.id}`) // RSpec provides test ids like "file_name.rb[1:2:3]". // This uses the digits at the end of the id to create @@ -165,6 +176,7 @@ export class TestLoader implements vscode.Disposable { } let childTestItem = this.testSuite.getOrCreateTestItem(test.id) + childTestItem.canResolveChildren = false childTestItem.label = description childTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); @@ -191,6 +203,7 @@ export class TestLoader implements vscode.Disposable { * Create a file watcher that will reload the test tree when a relevant file is changed. */ public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { + let log = this.log.getChildLogger({label: "parseTestsInFile"}) let testItem: vscode.TestItem if ("fsPath" in uri) { let test = this.testSuite.getTestItem(uri) @@ -202,7 +215,7 @@ export class TestLoader implements vscode.Disposable { testItem = uri } - this.log.info('A test file has been edited, reloading tests.'); + log.info(`${testItem.id} has been edited, reloading tests.`); await this.loadTests(testItem) } diff --git a/src/testRunner.ts b/src/testRunner.ts index 563955d..06d66a9 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; +import * as path from 'path' import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; @@ -7,6 +8,7 @@ import { TestRunContext } from './testRunContext'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; import { TestSuite } from './testSuite'; +import { ParsedTest } from './testLoader'; export abstract class TestRunner implements vscode.Disposable { protected currentChildProcess: childProcess.ChildProcess | undefined; @@ -133,22 +135,31 @@ export abstract class TestRunner implements vscode.Disposable { this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); childProcessLogger.debug(data); + let markTestStatus = (fn: (test: vscode.TestItem) => void, testId: string) => { + if (testId.startsWith(`.${path.sep}`)) { + testId = testId.substring(2) + } + if (testId.startsWith(this.config.getTestDirectory())) { + testId = testId.replace(this.config.getTestDirectory(), '') + if (testId.startsWith(path.sep)) { + testId = testId.substring(1) + } + } + let test = this.testSuite.getOrCreateTestItem(testId) + context.passed(test) + } if (data.startsWith('PASSED:')) { data = data.replace('PASSED: ', ''); - let test = this.testSuite.getOrCreateTestItem(data) - context.passed(test) + markTestStatus(test => context.passed(test), data) } else if (data.startsWith('FAILED:')) { data = data.replace('FAILED: ', ''); - let test = this.testSuite.getOrCreateTestItem(data) - context.failed(test, "", "", 0) + markTestStatus(test => context.failed(test, "", "", 0), data) } else if (data.startsWith('RUNNING:')) { data = data.replace('RUNNING: ', ''); - let test = this.testSuite.getOrCreateTestItem(data) - context.started(test) + markTestStatus(test => context.started(test), data) } else if (data.startsWith('PENDING:')) { data = data.replace('PENDING: ', ''); - let test = this.testSuite.getOrCreateTestItem(data) - context.enqueued(test) + markTestStatus(test => context.enqueued(test), data) } if (data.includes('START_OF_TEST_JSON')) { resolve(data); @@ -226,7 +237,12 @@ export abstract class TestRunner implements vscode.Disposable { await this.runNode(test, context); - test.children.forEach(test => queue.push(test)); + test.children.forEach(test => { + if (test.id.endsWith('.rb')) { + // Only add files, not all the single test cases + queue.push(test) + } + }); } if (token.isCancellationRequested) { this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) @@ -288,64 +304,37 @@ export abstract class TestRunner implements vscode.Disposable { node: vscode.TestItem | null, context: TestRunContext ): Promise { + let log = this.log.getChildLogger({label: "runNode"}) // Special case handling for the root suite, since it can be run // with runFullTestSuite() try { if (node == null) { + log.debug("Running all tests") this.controller.items.forEach((testSuite) => { this.enqueTestAndChildren(testSuite, context) }) let testOutput = await this.runFullTestSuite(context); - testOutput = TestRunner.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string; }) => { - this.handleStatus(test, context); - }); - } + this.parseAndHandleTestOutput(testOutput, context) // If the suite is a file, run the tests as a file rather than as separate tests. } else if (node.label.endsWith('.rb')) { + log.debug(`Running test file: ${node.id}`) // Mark selected tests as enqueued this.enqueTestAndChildren(node, context) context.started(node) - let testOutput = await this.runTestFile(`${node.uri?.fsPath}`, context); - - testOutput = TestRunner.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - if (tests && tests.length > 0) { - tests.forEach((test: { id: string }) => { - this.handleStatus(test, context); - }); - } - - if (tests.length != node.children.size + 1) { - this.log.debug(`Test count mismatch {${node.label}}. Expected ${node.children.size + 1}, ran ${tests.length}`) - } + let testOutput = await this.runTestFile(node, context); + this.parseAndHandleTestOutput(testOutput, context) } else { if (node.uri !== undefined && node.range !== undefined) { + log.debug(`Running single test: ${node.id}`) context.started(node) // Run the test at the given line, add one since the line is 0-indexed in // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(`${node.uri.fsPath}:${node.range?.end.line}`, context); + let testOutput = await this.runSingleTest(node, context); - testOutput = TestRunner.getJsonFromOutput(testOutput); - this.log.debug('Parsing the below JSON:'); - this.log.debug(`${testOutput}`); - let testMetadata = JSON.parse(testOutput); - let currentTest = testMetadata.examples[0]; - - this.handleStatus(currentTest, context); + this.parseAndHandleTestOutput(testOutput, context) } } } finally { @@ -353,6 +342,22 @@ export abstract class TestRunner implements vscode.Disposable { } } + private parseAndHandleTestOutput(testOutput: string, context: TestRunContext) { + let log = this.log.getChildLogger({label: 'parseAndHandleTestOutput'}) + testOutput = TestRunner.getJsonFromOutput(testOutput); + log.debug('Parsing the below JSON:'); + log.debug(`${testOutput}`); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; + + if (tests && tests.length > 0) { + tests.forEach((test: ParsedTest) => { + test.id = this.testSuite.normaliseTestId(test.id) + this.handleStatus(test, context); + }); + } + } + public async debugCommandStarted(): Promise { return new Promise(async (resolve, reject) => { this.debugCommandStartedResolver = resolve; @@ -364,6 +369,8 @@ export abstract class TestRunner implements vscode.Disposable { * Mark a test node and all its children as being queued for execution */ private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { + let log = this.log.getChildLogger({label: "enqueTestAndChildren"}) + log.debug(`enqueing test ${test.id}`) context.enqueued(test); if (test.children && test.children.size > 0) { test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) @@ -422,10 +429,10 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test. */ - protected async runSingleTest(testLocation: string, context: TestRunContext): Promise { - this.log.info(`Running single test: ${testLocation}`); + protected async runSingleTest(testItem: vscode.TestItem, context: TestRunContext): Promise { + this.log.info(`Running single test: ${testItem.id}`); return await this.runTestFramework( - this.getSingleTestCommand(testLocation, context), + this.getSingleTestCommand(testItem, context), "single test", context) } @@ -437,10 +444,10 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the tests. */ - protected async runTestFile(testFile: string, context: TestRunContext): Promise { - this.log.info(`Running test file: ${testFile}`); + protected async runTestFile(testItem: vscode.TestItem, context: TestRunContext): Promise { + this.log.info(`Running test file: ${testItem}`); return await this.runTestFramework( - this.getTestFileCommand(testFile, context), + this.getTestFileCommand(testItem, context), "test file", context) } @@ -466,7 +473,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test. */ - protected abstract getSingleTestCommand(testLocation: string, context: TestRunContext): string; + protected abstract getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string; /** * Gets the command to run tests in a given file. @@ -475,7 +482,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the tests. */ - protected abstract getTestFileCommand(testFile: string, context: TestRunContext): string; + protected abstract getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string; /** * Gets the command to run the full test suite for the current workspace. @@ -491,5 +498,5 @@ export abstract class TestRunner implements vscode.Disposable { * @param test The test that we want to handle. * @param context Test run context */ - protected abstract handleStatus(test: any, context: TestRunContext): void; + protected abstract handleStatus(test: ParsedTest, context: TestRunContext): void; } diff --git a/src/testSuite.ts b/src/testSuite.ts index c63f504..9fd1f8e 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -5,6 +5,7 @@ import { Config } from './config'; export class TestSuite { private readonly log: IChildLogger; + private readonly locationPattern = /\[[0-9]*(?::[0-9]*)*\]$/ constructor( readonly rootLog: IChildLogger, @@ -17,6 +18,7 @@ export class TestSuite { public deleteTestItem(testId: string | vscode.Uri) { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.uriToTestId(testId) + log.debug(`Deleting test ${testId}`) let collection = this.getParentTestItemCollection(testId) let testItem = collection.get(testId) if (testItem) { @@ -34,11 +36,21 @@ export class TestSuite { public getOrCreateTestItem(testId: string | vscode.Uri): vscode.TestItem { let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) testId = this.uriToTestId(testId) - let collection = this.getParentTestItemCollection(testId) + if (testId.startsWith(`.${path.sep}`)) { + testId = testId.substring(2) + } + log.debug(`Looking for test ${testId}`) + let collection = this.getOrCreateParentTestItemCollection(testId) let testItem = collection.get(testId) if (!testItem) { // Create a basic test item with what little info we have to be filled in later - testItem = this.controller.createTestItem(testId, testId.substring(testId.lastIndexOf(path.sep)), vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId))); + let label = testId.substring(testId.lastIndexOf(path.sep) + 1) + if (this.locationPattern.test(testId)) { + label = this.getPlaceholderLabelForSingleTest(testId) + } + let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId.replace(this.locationPattern, ''))) + testItem = this.controller.createTestItem(testId, label, uri); + testItem.canResolveChildren = !this.locationPattern.test(testId) collection.add(testItem); log.debug(`Added test ${testItem.id}`) } @@ -48,7 +60,7 @@ export class TestSuite { public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: 'getTestItem'}) testId = this.uriToTestId(testId) - let collection = this.getParentTestItemCollection(testId) + let collection = this.getOrCreateParentTestItemCollection(testId) let testItem = collection.get(testId) if (!testItem) { log.debug(`Couldn't find ${testId}`) @@ -57,22 +69,42 @@ export class TestSuite { return testItem } + public normaliseTestId(testId: string): string { + if (testId.startsWith(`.${path.sep}`)) { + testId = testId.substring(2) + } + if (testId.startsWith(this.config.getTestDirectory())) { + testId = testId.replace(this.config.getTestDirectory(), '') + if (testId.startsWith(path.sep)) { + testId.substring(1) + } + } + return testId + } + private uriToTestId(uri: string | vscode.Uri): string { if (typeof uri === "string") return uri - return uri.fsPath.replace(path.resolve(this.config.getTestDirectory()), '') + return uri.fsPath.replace( + path.resolve( + vscode.workspace?.workspaceFolders![0].uri.fsPath, + this.config.getTestDirectory()) + path.sep, + '') } private getParentTestItemCollection(testId: string): vscode.TestItemCollection { testId = testId.replace(/^\.\/spec\//, '') let idSegments = testId.split(path.sep) + if (idSegments[0] === "") { + idSegments.splice(0, 1) + } let collection: vscode.TestItemCollection = this.controller.items // Walk the test hierarchy to find the collection containing our test file for (let i = 0; i < idSegments.length - 1; i++) { let collectionId = (i == 0) ? idSegments[0] - : idSegments.slice(0,i).join(path.sep) + : idSegments.slice(0, i + 1).join(path.sep) let childCollection = collection.get(collectionId)?.children if (!childCollection) { throw `Test collection not found: ${collectionId}` @@ -81,7 +113,7 @@ export class TestSuite { } // Need to make sure we strip locations from file id to get final collection - let fileId = testId.replace(/\[[0-9](?::[0-9])*\]$/, '') + let fileId = testId.replace(this.locationPattern, '') let childCollection = collection.get(fileId)?.children if (!childCollection) { throw `Test collection not found: ${fileId}` @@ -89,4 +121,59 @@ export class TestSuite { collection = childCollection return collection } + + private getOrCreateParentTestItemCollection(testId: string): vscode.TestItemCollection { + let log = this.log.getChildLogger({label: `getOrCreateParentTestItemCollection(${testId})`}) + testId = testId.replace(/^\.\/spec\//, '') + let idSegments = testId.split(path.sep) + log.debug('id segments', idSegments) + if (idSegments[0] === "") { + idSegments.splice(0, 1) + } + let collection: vscode.TestItemCollection = this.controller.items + + // Walk the test hierarchy to find the collection containing our test file + for (let i = 0; i < idSegments.length - 1; i++) { + let collectionId = (i == 0) + ? idSegments[0] + : idSegments.slice(0, i + 1).join(path.sep) + log.debug(`Getting parent collection ${collectionId}`) + let childCollection = collection.get(collectionId)?.children + if (!childCollection) { + log.debug(`${collectionId} not found, creating`) + let label = idSegments[i] + let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), collectionId)) + let child = this.controller.createTestItem(collectionId, label, uri) + child.canResolveChildren = true + collection.add(child) + childCollection = child.children + } + collection = childCollection + } + + if (this.locationPattern.test(testId)) { + // Test item is a test within a file + // Need to make sure we strip locations from file id to get final collection + let fileId = testId.replace(this.locationPattern, '') + if (fileId.startsWith(path.sep)) { + fileId = fileId.substring(1) + } + log.debug(`Getting file collection ${fileId}`) + let childCollection = collection.get(fileId)?.children + if (!childCollection) { + log.debug(`${fileId} not found, creating`) + let child = this.controller.createTestItem(fileId, fileId.substring(fileId.lastIndexOf(path.sep) + 1), vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId))) + child.canResolveChildren = true + collection.add(child) + childCollection = child.children + } + collection = childCollection + } + // else test item is the file so return the file's parent + return collection + } + + private getPlaceholderLabelForSingleTest(testId: string): string { + return `Awaiting test details... (location: ${this.locationPattern.exec(testId)})` + } } \ No newline at end of file From 5686bbe4851f7743f42b95291d13e84225d824a7 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:00:46 +0000 Subject: [PATCH 026/108] Update TS test stubs of vscode Test API interfaces to match upstream changes --- test/stubs/stubTestController.ts | 2 + test/stubs/stubTestItemCollection.ts | 57 +++++++++++++--------------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts index 97483e5..1fb1192 100644 --- a/test/stubs/stubTestController.ts +++ b/test/stubs/stubTestController.ts @@ -21,6 +21,8 @@ export class StubTestController implements vscode.TestController { resolveHandler?: ((item: vscode.TestItem | undefined) => void | Thenable) | undefined; + refreshHandler: ((token: vscode.CancellationToken) => void | Thenable) | undefined; + createTestRun( request: vscode.TestRunRequest, name?: string, diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index adbdf08..62dab44 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -1,54 +1,49 @@ import * as vscode from 'vscode' export class StubTestItemCollection implements vscode.TestItemCollection { - private testIds: { [name: string]: number } = {}; - private data: vscode.TestItem[] = [] - size: number = 0; + private testIds: { [name: string]: vscode.TestItem } = {}; + get size(): number { return Object.keys(this.testIds).length; }; replace(items: readonly vscode.TestItem[]): void { - this.data = [] items.forEach(item => { - this.testIds[item.id] = this.data.length - this.data.push(item) + this.testIds[item.id] = item }) - this.size = this.data.length; } forEach(callback: (item: vscode.TestItem, collection: vscode.TestItemCollection) => unknown, thisArg?: unknown): void { - this.data.forEach((element: vscode.TestItem) => { + Object.values(this.testIds).forEach((element: vscode.TestItem) => { return callback(element, this) }); } + [Symbol.iterator](): Iterator<[id: string, testItem: vscode.TestItem]> { + let step = 0; + const iterator = { + next(): IteratorResult<[id: string, testItem: vscode.TestItem]> { + let testId = Object.keys(super.testIds)[step]; + let value: [id: string, testItem: vscode.TestItem] = [ + testId, + super.testIds[testId] + ]; + step++; + return { + value: value, + done: step >= super.size + } + } + } + return iterator; + } + add(item: vscode.TestItem): void { - this.testIds[item.id] = this.data.length - this.data.push(item) - this.size++ + this.testIds[item.id] = item } delete(itemId: string): void { - let index = this.testIds[itemId] - if (index !== undefined || -1) { - this.data.splice(index) - delete this.testIds[itemId] - this.size-- - } + delete this.testIds[itemId] } get(itemId: string): vscode.TestItem | undefined { - let item: vscode.TestItem | undefined = undefined - this.data.forEach((child) => { - if (!item) { - if (child.id === itemId) { - item = child - } else { - let result = child.children.get(itemId) - if (result) { - item = result - } - } - } - }) - return item + return this.testIds[itemId] } } \ No newline at end of file From cab32d9eebcb4d0710a768a769c9cb6cd2c17edf Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:00:47 +0000 Subject: [PATCH 027/108] Allow VSCode to run automatic npm tasks --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c715b5..12a57ed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ "ruby.format": false, "ruby.lint": { "rubocop": false - } + }, + "task.allowAutomaticTasks": "on" } From 7adf9d0ec4be70c50a5b72565b3acbeab7d0bac0 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:00:47 +0000 Subject: [PATCH 028/108] Add .bundle folders to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8e77041..2bc19e0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ruby/Gemfile.lock *.vsix .rbenv-gemsets .ruby-version +**/.bundle/ From 60cbd42bc7ab0cf760509e7a07fc039af7063f2e Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:00:47 +0000 Subject: [PATCH 029/108] Add .nvmrc set to use LTS version of node --- .nvmrc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ab2e7f4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +lts/* + From c02bc91995e0e73a37396ad45415aff4b036899f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:00:47 +0000 Subject: [PATCH 030/108] Fix failing unit tests --- src/rspec/rspecConfig.ts | 9 +++++++-- test/suite/unitTests/config.test.ts | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 222907f..145407f 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -3,6 +3,8 @@ import * as vscode from 'vscode' import * as path from 'path'; export class RspecConfig extends Config { + readonly DEFAULT_TEST_DIRECTORY = 'spec' + public frameworkName(): string { return "RSpec" } @@ -81,9 +83,12 @@ export class RspecConfig extends Config { } public getTestDirectory(): string { - let configDir = vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecDirectory') as string + let configDir = vscode.workspace.getConfiguration('rubyTestExplorer').get('rspecDirectory') as string + if (!configDir) + return this.DEFAULT_TEST_DIRECTORY + if (configDir.startsWith('./')) configDir = configDir.substring(2) - return configDir ?? `spec`; + return configDir ?? this.DEFAULT_TEST_DIRECTORY; } } \ No newline at end of file diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index 43460a0..d4e556e 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -55,13 +55,13 @@ suite('Config', function() { let config = new RspecConfig("../../../ruby") expect(config.getTestCommandWithFilePattern()).to - .eq("bundle exec rspec --pattern './spec/**/*_test.rb,./spec/**/test_*.rb'") + .eq("bundle exec rspec --pattern 'spec/**/*_test.rb,spec/**/test_*.rb'") }) suite("#getTestDirectory()", function() { - test("with no config set, it returns ./spec", function() { + test("with no config set, it returns default value", function() { let config = new RspecConfig("../../../ruby") - expect(config.getTestDirectory()).to.eq("./spec") + expect(config.getTestDirectory()).to.eq("spec/") }) }) }) From 481fa3b590a56d1fbca0182b3eea69d8a6ec9e52 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 15:30:40 +0000 Subject: [PATCH 031/108] Start trying to fix rspec test failures --- src/testLoader.ts | 33 +++++++++---- src/testSuite.ts | 33 +++++++++---- test/suite/rspec/rspec.test.ts | 86 +++++++++++++++++----------------- 3 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index 1760943..e902da5 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -18,6 +18,13 @@ export type ParsedTest = { pending_message?: string, exception?: any, } + +/** + * Responsible for finding and watching test files, and loading tests from within those + * files + * + * Defers to TestSuite for parsing test information + */ export class TestLoader implements vscode.Disposable { protected disposables: { dispose(): void }[] = []; private readonly log: IChildLogger; @@ -41,6 +48,12 @@ export class TestLoader implements vscode.Disposable { this.disposables = []; } + /** + * Create a file watcher that will update the test tree when: + * - A test file is created + * - A test file is changed + * - A test file is deleted + */ async createWatcher(pattern: vscode.GlobPattern): Promise { let log = this.log.getChildLogger({label: `createWatcher(${pattern})`}) const watcher = vscode.workspace.createFileSystemWatcher(pattern); @@ -70,6 +83,10 @@ export class TestLoader implements vscode.Disposable { return watcher; } + /** + * Searches the configured test directory for test files, and calls createWatcher for + * each one found. + */ discoverAllFilesInWorkspace() { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) let testDir = path.resolve(this.workspace?.uri.fsPath ?? '.', this.config.getTestDirectory()) @@ -103,7 +120,7 @@ export class TestLoader implements vscode.Disposable { log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); output = TestRunner.getJsonFromOutput(output); - log.debug(`Parsing the returnd JSON: ${output}`); + log.debug(`Parsing the returned JSON: ${output}`); let testMetadata; try { testMetadata = JSON.parse(output); @@ -117,8 +134,8 @@ export class TestLoader implements vscode.Disposable { (test: ParsedTest) => { let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); - test.id = test.id.replace(this.config.getTestDirectory(), '') + test.location = parseInt(test_location_string); // TODO: check this isn't RSpec specific + test.id = this.testSuite.normaliseTestId(test.id) test.file_path = test.file_path.replace(this.config.getTestDirectory(), '') tests.push(test); log.debug("Parsed test", test) @@ -126,7 +143,7 @@ export class TestLoader implements vscode.Disposable { ); log.debug("Test output parsed. Adding tests to test suite", tests) - await this.getTestSuiteForFile(tests, testItem); + this.getTestSuiteForFile(tests, testItem); } catch (e: any) { log.error("Failed to load tests", e) return Promise.reject(e) @@ -137,14 +154,14 @@ export class TestLoader implements vscode.Disposable { * Get the tests in a given file. * * @param tests Parsed output from framework - * @param currentFile Name of the file we're checking for tests - * @param dirPath Full path to the root test folder + * @param testItem TestItem object containing file details */ public getTestSuiteForFile(tests: Array, testItem: vscode.TestItem) { let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${testItem.id})` }) let currentFileSplitName = testItem.uri?.fsPath.split(path.sep); let currentFileLabel = currentFileSplitName ? currentFileSplitName[currentFileSplitName!.length - 1] : testItem.label + log.debug(`Current file label: ${currentFileLabel}`) let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); @@ -199,9 +216,7 @@ export class TestLoader implements vscode.Disposable { return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); } - /** - * Create a file watcher that will reload the test tree when a relevant file is changed. - */ + public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { let log = this.log.getChildLogger({label: "parseTestsInFile"}) let testItem: vscode.TestItem diff --git a/src/testSuite.ts b/src/testSuite.ts index 9fd1f8e..6775b61 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -49,6 +49,7 @@ export class TestSuite { label = this.getPlaceholderLabelForSingleTest(testId) } let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId.replace(this.locationPattern, ''))) + log.debug(`Creating test item - id: ${testId}, label: ${label}, uri: ${uri}`) testItem = this.controller.createTestItem(testId, label, uri); testItem.canResolveChildren = !this.locationPattern.test(testId) collection.add(testItem); @@ -69,6 +70,12 @@ export class TestSuite { return testItem } + /** + * Takes a test ID from the test runner output and normalises it to a consistent format + * + * - Removes leading './' if present + * - Removes leading test dir if present + */ public normaliseTestId(testId: string): string { if (testId.startsWith(`.${path.sep}`)) { testId = testId.substring(2) @@ -83,13 +90,19 @@ export class TestSuite { } private uriToTestId(uri: string | vscode.Uri): string { - if (typeof uri === "string") + let log = this.log.getChildLogger({label: `uriToTestId(${uri})`}) + if (typeof uri === "string") { + log.debug("uri is string. Returning unchanged") return uri - return uri.fsPath.replace( - path.resolve( - vscode.workspace?.workspaceFolders![0].uri.fsPath, - this.config.getTestDirectory()) + path.sep, - '') + } + let fullTestDirPath = path.resolve( + vscode.workspace?.workspaceFolders![0].uri.fsPath, + this.config.getTestDirectory() + ) + log.debug(`Full path to test dir: ${fullTestDirPath}`) + let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') + log.debug(`Stripped URI: ${strippedUri}`) + return strippedUri } private getParentTestItemCollection(testId: string): vscode.TestItemCollection { @@ -140,9 +153,9 @@ export class TestSuite { log.debug(`Getting parent collection ${collectionId}`) let childCollection = collection.get(collectionId)?.children if (!childCollection) { - log.debug(`${collectionId} not found, creating`) let label = idSegments[i] let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), collectionId)) + log.debug(`${collectionId} not found, creating - label: ${label}, uri: ${uri}`) let child = this.controller.createTestItem(collectionId, label, uri) child.canResolveChildren = true collection.add(child) @@ -161,8 +174,10 @@ export class TestSuite { log.debug(`Getting file collection ${fileId}`) let childCollection = collection.get(fileId)?.children if (!childCollection) { - log.debug(`${fileId} not found, creating`) - let child = this.controller.createTestItem(fileId, fileId.substring(fileId.lastIndexOf(path.sep) + 1), vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId))) + let label = fileId.substring(fileId.lastIndexOf(path.sep) + 1) + let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId)) + log.debug(`${fileId} not found, creating - label: ${label}, uri: ${uri}`) + let child = this.controller.createTestItem(fileId, label, uri) child.canResolveChildren = true collection.add(child) childCollection = child.children diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 368d034..c47775d 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -7,7 +7,7 @@ import { TestLoader } from '../../../src/testLoader'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { StubTestController } from '../../stubs/stubTestController'; import { expect } from 'chai'; -import { TestSuite } from 'src/testSuite'; +import { TestSuite } from '../../../src/testSuite'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -49,19 +49,6 @@ suite('Extension Test for RSpec', function() { // No tests in suite initially, only test files and folders testItemCollectionMatches(testController.items, [ - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", - children: [] - } - ] - }, { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", @@ -74,15 +61,6 @@ suite('Extension Test for RSpec', function() { label: "square_spec.rb", children: [] }, - ] - ) - - // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - - // Tests in that file have now been added to suite - testItemCollectionMatches(testController.items, - [ { file: expectedPath("subfolder"), id: "subfolder", @@ -96,6 +74,15 @@ suite('Extension Test for RSpec', function() { } ] }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", @@ -127,6 +114,19 @@ suite('Extension Test for RSpec', function() { label: "square_spec.rb", children: [] }, + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [] + } + ] + }, ] ) }) @@ -142,26 +142,6 @@ suite('Extension Test for RSpec', function() { testItemCollectionMatches(testSuite, [ - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb[1:1]", - label: "wibbles and wobbles", - line: 3, - } - ] - } - ] - }, { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", @@ -206,6 +186,26 @@ suite('Extension Test for RSpec', function() { } ] }, + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb[1:1]", + label: "wibbles and wobbles", + line: 3, + } + ] + } + ] + }, ] ) }) From 998caef7ba27901a84c991e56c5117a5a752e0dc Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 20:54:23 +0000 Subject: [PATCH 032/108] Cleanup and refactor TestSuite and start unit testing it --- src/testRunContext.ts | 10 +- src/testRunner.ts | 4 +- src/testSuite.ts | 177 ++++++++++++++++--------- test/stubs/stubTestController.ts | 3 +- test/stubs/stubTestItem.ts | 1 + test/suite/helpers.ts | 3 +- test/suite/rspec/rspec.test.ts | 13 +- test/suite/unitTests/config.test.ts | 5 +- test/suite/unitTests/testSuite.test.ts | 154 +++++++++++++++++++++ 9 files changed, 288 insertions(+), 82 deletions(-) create mode 100644 test/suite/unitTests/testSuite.test.ts diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 08d93ee..7628c8d 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -33,7 +33,7 @@ export class TestRunContext { /** * Indicates a test is queued for later execution. * - * @param testId ID of the test item to update. + * @param test Test item to update. */ public enqueued(test: vscode.TestItem): void { this.testRun.enqueued(test) @@ -45,7 +45,7 @@ export class TestRunContext { * * This differs from the "failed" state in that it indicates a test that couldn't be executed at all, from a compilation error for example * - * @param testId ID of the test item to update. + * @param test Test item to update. * @param message Message(s) associated with the test failure. * @param duration How long the test took to execute, in milliseconds. */ @@ -73,7 +73,7 @@ export class TestRunContext { /** * Indicates a test has failed. * - * @param testId ID of the test item to update. + * @param test Test item to update. * @param message Message(s) associated with the test failure. * @param file Path to the file containing the failed test * @param line Line number where the error occurred @@ -98,7 +98,7 @@ export class TestRunContext { /** * Indicates a test has passed. * - * @param testId ID of the test item to update. + * @param test Test item to update. * @param duration How long the test took to execute, in milliseconds. */ public passed(test: vscode.TestItem, @@ -121,7 +121,7 @@ export class TestRunContext { /** * Indicates a test has started running. * - * @param testId ID of the test item to update, or the test item. + * @param test Test item to update, or the test item. */ public started(test: vscode.TestItem): void { this.testRun.started(test) diff --git a/src/testRunner.ts b/src/testRunner.ts index 06d66a9..2180c4c 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -17,9 +17,11 @@ export abstract class TestRunner implements vscode.Disposable { protected readonly log: IChildLogger; /** - * @param log The Test Adapter logger, for logging. + * @param rootLog The Test Adapter logger, for logging. * @param workspace Open workspace folder * @param controller Test controller that holds the test suite + * @param config Configuration provider + * @param testSuite TestSuite instance */ constructor( rootLog: IChildLogger, diff --git a/src/testSuite.ts b/src/testSuite.ts index 6775b61..4c8ee05 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -3,6 +3,11 @@ import path from 'path' import { IChildLogger } from '@vscode-logging/logger'; import { Config } from './config'; +/** + * Manages the contents of the test suite + * + * Responsible for creating, deleting and finding test items + */ export class TestSuite { private readonly log: IChildLogger; private readonly locationPattern = /\[[0-9]*(?::[0-9]*)*\]$/ @@ -19,7 +24,11 @@ export class TestSuite { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.uriToTestId(testId) log.debug(`Deleting test ${testId}`) - let collection = this.getParentTestItemCollection(testId) + let collection = this.getParentTestItemCollection(testId, false) + if (!collection) { + log.debug('Parent test collection not found') + return + } let testItem = collection.get(testId) if (testItem) { collection.delete(testItem.id); @@ -40,7 +49,7 @@ export class TestSuite { testId = testId.substring(2) } log.debug(`Looking for test ${testId}`) - let collection = this.getOrCreateParentTestItemCollection(testId) + let collection = this.getParentTestItemCollection(testId, true)! let testItem = collection.get(testId) if (!testItem) { // Create a basic test item with what little info we have to be filled in later @@ -48,21 +57,27 @@ export class TestSuite { if (this.locationPattern.test(testId)) { label = this.getPlaceholderLabelForSingleTest(testId) } - let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId.replace(this.locationPattern, ''))) - log.debug(`Creating test item - id: ${testId}, label: ${label}, uri: ${uri}`) - testItem = this.controller.createTestItem(testId, label, uri); - testItem.canResolveChildren = !this.locationPattern.test(testId) - collection.add(testItem); - log.debug(`Added test ${testItem.id}`) + testItem = this.createTestItem( + collection, + testId, + label, + vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId.replace(this.locationPattern, ''))), + !this.locationPattern.test(testId) + ); } return testItem } + /** + * Gets a TestItem from the list of tests + * @param testId ID of the TestItem to get + * @returns TestItem if found, else undefined + */ public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: 'getTestItem'}) testId = this.uriToTestId(testId) - let collection = this.getOrCreateParentTestItemCollection(testId) - let testItem = collection.get(testId) + let collection = this.getParentTestItemCollection(testId, false) + let testItem = collection?.get(testId) if (!testItem) { log.debug(`Couldn't find ${testId}`) return undefined @@ -83,12 +98,17 @@ export class TestSuite { if (testId.startsWith(this.config.getTestDirectory())) { testId = testId.replace(this.config.getTestDirectory(), '') if (testId.startsWith(path.sep)) { - testId.substring(1) + testId = testId.substring(1) } } return testId } + /** + * Converts a test URI into a test ID + * @param uri URI of test + * @returns test ID + */ private uriToTestId(uri: string | vscode.Uri): string { let log = this.log.getChildLogger({label: `uriToTestId(${uri})`}) if (typeof uri === "string") { @@ -105,65 +125,36 @@ export class TestSuite { return strippedUri } - private getParentTestItemCollection(testId: string): vscode.TestItemCollection { - testId = testId.replace(/^\.\/spec\//, '') - let idSegments = testId.split(path.sep) - if (idSegments[0] === "") { - idSegments.splice(0, 1) - } - let collection: vscode.TestItemCollection = this.controller.items - - // Walk the test hierarchy to find the collection containing our test file - for (let i = 0; i < idSegments.length - 1; i++) { - let collectionId = (i == 0) - ? idSegments[0] - : idSegments.slice(0, i + 1).join(path.sep) - let childCollection = collection.get(collectionId)?.children - if (!childCollection) { - throw `Test collection not found: ${collectionId}` - } - collection = childCollection - } - - // Need to make sure we strip locations from file id to get final collection - let fileId = testId.replace(this.locationPattern, '') - let childCollection = collection.get(fileId)?.children - if (!childCollection) { - throw `Test collection not found: ${fileId}` - } - collection = childCollection - return collection - } - - private getOrCreateParentTestItemCollection(testId: string): vscode.TestItemCollection { - let log = this.log.getChildLogger({label: `getOrCreateParentTestItemCollection(${testId})`}) - testId = testId.replace(/^\.\/spec\//, '') - let idSegments = testId.split(path.sep) - log.debug('id segments', idSegments) - if (idSegments[0] === "") { - idSegments.splice(0, 1) - } + /** + * Searches the collection of tests for the TestItemCollection that contains the given test ID + * @param testId ID of the test to get the parent collection of + * @param createIfMissing Create parent test collections if missing + * @returns Parent collection of the given test ID + */ + private getParentTestItemCollection(testId: string, createIfMissing: boolean): vscode.TestItemCollection | undefined { + let log = this.log.getChildLogger({label: `getParentTestItemCollection(${testId}, createIfMissing: ${createIfMissing})`}) + let idSegments = this.splitTestId(testId) let collection: vscode.TestItemCollection = this.controller.items - // Walk the test hierarchy to find the collection containing our test file + // Walk through test folders to find the collection containing our test file for (let i = 0; i < idSegments.length - 1; i++) { - let collectionId = (i == 0) - ? idSegments[0] - : idSegments.slice(0, i + 1).join(path.sep) + let collectionId = this.getPartialId(idSegments, i) log.debug(`Getting parent collection ${collectionId}`) let childCollection = collection.get(collectionId)?.children if (!childCollection) { - let label = idSegments[i] - let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), collectionId)) - log.debug(`${collectionId} not found, creating - label: ${label}, uri: ${uri}`) - let child = this.controller.createTestItem(collectionId, label, uri) - child.canResolveChildren = true - collection.add(child) + if (!createIfMissing) return undefined + let child = this.createTestItem( + collection, + collectionId, + idSegments[i], + vscode.Uri.file(path.resolve(this.config.getTestDirectory(), collectionId)) + ) childCollection = child.children } collection = childCollection } + // TODO: This might not handle nested describe/context/etc blocks? if (this.locationPattern.test(testId)) { // Test item is a test within a file // Need to make sure we strip locations from file id to get final collection @@ -174,12 +165,13 @@ export class TestSuite { log.debug(`Getting file collection ${fileId}`) let childCollection = collection.get(fileId)?.children if (!childCollection) { - let label = fileId.substring(fileId.lastIndexOf(path.sep) + 1) - let uri = vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId)) - log.debug(`${fileId} not found, creating - label: ${label}, uri: ${uri}`) - let child = this.controller.createTestItem(fileId, label, uri) - child.canResolveChildren = true - collection.add(child) + if (!createIfMissing) return undefined + let child = this.createTestItem( + collection, + fileId, + fileId.substring(fileId.lastIndexOf(path.sep) + 1), + vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId)) + ) childCollection = child.children } collection = childCollection @@ -188,7 +180,60 @@ export class TestSuite { return collection } + /** + * Creates a TestItem and adds it to a TestItemCollection + * @param collection + * @param testId + * @param label + * @param uri + * @param canResolveChildren + * @returns + */ + private createTestItem( + collection: vscode.TestItemCollection, + testId: string, + label: string, + uri: vscode.Uri, + canResolveChildren: boolean = true + ): vscode.TestItem { + let log = this.log.getChildLogger({ label: `createTestId(${testId})` }) + log.debug(`Creating test item - label: ${label}, uri: ${uri}, canResolveChildren: ${canResolveChildren}`) + let item = this.controller.createTestItem(testId, label, uri) + item.canResolveChildren = canResolveChildren + collection.add(item); + log.debug(`Added test ${item.id}`) + return item + } + + /** + * Builds the testId of a parent folder from the parts of a child ID up to the given depth + * @param idSegments array of segments of a test ID (e.g. ['foo', 'bar', 'bat.rb'] would be the segments for the test item 'foo/bar/bat.rb') + * @param depth number of segments to use to build the ID + * @returns test ID of a parent folder + */ + private getPartialId(idSegments: string[], depth: number): string { + return (depth == 0) + ? idSegments[0] + : idSegments.slice(0, depth + 1).join(path.sep) + } + + /** + * Splits a test ID into segments by path separator + * @param testId + * @returns + */ + private splitTestId(testId: string): string[] { + let log = this.log.getChildLogger({label: `splitTestId(${testId})`}) + testId = this.normaliseTestId(testId) + let idSegments = testId.split(path.sep) + log.debug('id segments', idSegments) + if (idSegments[0] === "") { + idSegments.splice(0, 1) + } + return idSegments + } + private getPlaceholderLabelForSingleTest(testId: string): string { return `Awaiting test details... (location: ${this.locationPattern.exec(testId)})` } -} \ No newline at end of file +} diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts index 1fb1192..11ae447 100644 --- a/test/stubs/stubTestController.ts +++ b/test/stubs/stubTestController.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode' +import { instance, mock } from 'ts-mockito'; + import { StubTestItemCollection } from './stubTestItemCollection'; import { StubTestItem } from './stubTestItem'; -import { instance, mock } from 'ts-mockito'; export class StubTestController implements vscode.TestController { id: string = "stub_test_controller_id"; diff --git a/test/stubs/stubTestItem.ts b/test/stubs/stubTestItem.ts index 03c4bd1..be89199 100644 --- a/test/stubs/stubTestItem.ts +++ b/test/stubs/stubTestItem.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode' + import { StubTestItemCollection } from './stubTestItemCollection'; export class StubTestItem implements vscode.TestItem { diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index a0efd22..dc647a2 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,10 +1,11 @@ import * as vscode from 'vscode' import { expect } from 'chai' import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; -import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; +import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; + export function noop() {} /** diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index c47775d..8803333 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,13 +1,14 @@ import * as vscode from 'vscode'; import * as path from 'path' import { anything, instance, verify } from 'ts-mockito' -import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; -import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; -import { TestLoader } from '../../../src/testLoader'; -import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { StubTestController } from '../../stubs/stubTestController'; import { expect } from 'chai'; -import { TestSuite } from '../../../src/testSuite'; + +import { RspecTestRunner } from 'src/rspec/rspecTestRunner'; +import { TestLoader } from 'src/testLoader'; +import { RspecConfig } from 'src/rspec/rspecConfig'; +import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from 'test/suite/helpers'; +import { StubTestController } from 'test/stubs/stubTestController'; +import { TestSuite } from 'src/testSuite'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index d4e556e..dcfee43 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -1,9 +1,10 @@ -import { noop_logger } from "../helpers"; +import { expect } from "chai"; import { spy, when } from 'ts-mockito' import * as vscode from 'vscode' + import { Config } from "../../../src/config"; import { RspecConfig } from "../../../src/rspec/rspecConfig"; -import { expect } from "chai"; +import { noop_logger } from "../helpers"; suite('Config', function() { let setConfig = (testFramework: string) => { diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts new file mode 100644 index 0000000..623bc35 --- /dev/null +++ b/test/suite/unitTests/testSuite.test.ts @@ -0,0 +1,154 @@ +import { expect } from 'chai'; +import { before, beforeEach } from 'mocha'; +import { instance, mock, when } from 'ts-mockito' +import * as vscode from 'vscode' + +import { Config } from '../../../src/config'; +import { TestSuite } from '../../../src/testSuite'; +import { StubTestController } from '../../stubs/stubTestController'; +import { StubTestItem } from '../../stubs/stubTestItem'; +import { noop_logger } from '../helpers'; + +suite('TestSuite', function () { + let config: Config = mock(); + let controller: vscode.TestController; + let testSuite: TestSuite; + + before(function () { + when(config.getTestDirectory()).thenReturn('spec') + }); + + beforeEach(function () { + controller = new StubTestController() + testSuite = new TestSuite(noop_logger(), controller, instance(config)) + }); + + suite('#normaliseTestId()', function () { + const parameters = [ + { arg: 'test-id', expected: 'test-id' }, + { arg: './test-id', expected: 'test-id' }, + { arg: 'folder/test-id', expected: 'folder/test-id' }, + { arg: './folder/test-id', expected: 'folder/test-id' }, + { arg: 'spec/test-id', expected: 'test-id' }, + { arg: './spec/test-id', expected: 'test-id' }, + { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './spec/folder/test-id', expected: 'folder/test-id' }, + ]; + + parameters.forEach(({ arg, expected }) => { + test(`correctly normalises ${arg} to ${expected}`, function () { + expect(testSuite.normaliseTestId(arg)).to.eq(expected); + }); + }); + }); + + suite('#deleteTestItem()', function () { + const id = 'test-id' + const label = 'test-label' + + beforeEach(function () { + controller.items.add(new StubTestItem(id, label)) + }) + + test('deletes only the specified test item', function () { + let secondTestItem = new StubTestItem('test-id-2', 'test-label-2') + controller.items.add(secondTestItem) + expect(controller.items.size).to.eq(2) + + testSuite.deleteTestItem(id) + + expect(controller.items.size).to.eq(1) + expect(controller.items.get('test-id-2')).to.eq(secondTestItem) + }) + + test('does nothing if ID not found', function () { + expect(controller.items.size).to.eq(1) + + testSuite.deleteTestItem('test-id-2') + + expect(controller.items.size).to.eq(1) + }) + }); + + suite('#getTestItem()', function () { + const id = 'test-id' + const label = 'test-label' + const testItem = new StubTestItem(id, label) + const childId = 'folder/child-test' + const childItem = new StubTestItem(childId, 'child-test') + const folderItem = new StubTestItem('folder', 'folder') + + beforeEach(function () { + controller.items.add(testItem) + folderItem.children.add(childItem) + controller.items.add(folderItem) + }) + + test('gets the specified test if ID is found', function () { + expect(testSuite.getTestItem(id)).to.eq(testItem) + }) + + test('returns undefined if ID is not found', function () { + expect(testSuite.getTestItem('not-found')).to.be.undefined + }) + + test('gets the specified nested test if ID is found', function () { + expect(testSuite.getTestItem(childId)).to.eq(childItem) + }) + + test('returns undefined if nested ID is not found', function () { + expect(testSuite.getTestItem('folder/not-found')).to.be.undefined + }) + + test('returns undefined if parent of nested ID is not found', function () { + expect(testSuite.getTestItem('not-found/child-test')).to.be.undefined + }) + }) + + suite('#getOrCreateTestItem()', function () { + const id = 'test-id' + const label = 'test-label' + const testItem = new StubTestItem(id, label) + const childId = 'folder/child-test' + const childItem = new StubTestItem(childId, 'child-test') + + test('gets the specified item if ID is found', function () { + controller.items.add(testItem) + expect(testSuite.getOrCreateTestItem(id)).to.eq(testItem) + }) + + test('creates item if ID is not found', function () { + let testItem = testSuite.getOrCreateTestItem('not-found') + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq('not-found') + }) + + test('gets the specified nested test if ID is found', function () { + let folderItem = new StubTestItem('folder', 'folder') + controller.items.add(testItem) + folderItem.children.add(childItem) + controller.items.add(folderItem) + + expect(testSuite.getOrCreateTestItem(childId)).to.eq(childItem) + }) + + test('creates item if nested ID is not found', function () { + let folderItem = new StubTestItem('folder', 'folder') + controller.items.add(folderItem) + + let testItem = testSuite.getOrCreateTestItem('folder/not-found') + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq('folder/not-found') + }) + + test('creates item and parent if parent of nested ID is not found', function () { + let testItem = testSuite.getOrCreateTestItem('folder/not-found') + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq('folder/not-found') + + let folder = testSuite.getOrCreateTestItem('folder') + expect(folder?.children.size).to.eq(1) + expect(folder?.children.get('folder/not-found')).to.eq(testItem) + }) + }) +}); From 3beb215d04ebe7025ce2f3b7dfe45de5c3368005 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 8 Nov 2022 22:06:29 +0000 Subject: [PATCH 033/108] Add .editorconfig file --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true From 9cbf25bb75e9f4542e3dcea273f22679f2a37809 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 9 Nov 2022 21:23:18 +0000 Subject: [PATCH 034/108] Add some TestLoader test and fix up some parsing bugs --- src/testLoader.ts | 67 ++++-- src/testSuite.ts | 6 + test/suite/unitTests/testLoader.test.ts | 300 +++++++----------------- test/suite/unitTests/testSuite.test.ts | 17 +- 4 files changed, 151 insertions(+), 239 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index e902da5..fc11a1d 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -13,10 +13,11 @@ export type ParsedTest = { description: string, file_path: string, line_number: number, - location: number, + location?: number, status?: string, - pending_message?: string, + pending_message?: string | null, exception?: any, + type?: any, // what is this? } /** @@ -37,8 +38,10 @@ export class TestLoader implements vscode.Disposable { private readonly config: Config, private readonly testSuite: TestSuite, ) { - this.log = rootLog.getChildLogger({label: "TestLoader"}); + this.log = rootLog.getChildLogger({ label: "TestLoader" }); + this.log.debug('constructor') this.disposables.push(this.configWatcher()); + this.log.debug('constructor complete') } dispose(): void { @@ -90,6 +93,7 @@ export class TestLoader implements vscode.Disposable { discoverAllFilesInWorkspace() { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) let testDir = path.resolve(this.workspace?.uri.fsPath ?? '.', this.config.getTestDirectory()) + log.debug(`testDir: ${testDir}`) let patterns: Array = [] this.config.getFilePattern().forEach(pattern => { @@ -104,6 +108,8 @@ export class TestLoader implements vscode.Disposable { log.debug("Setting up watchers with the following test patterns", patterns) return Promise.all(patterns.map(async (pattern) => await this.createWatcher(pattern))) + .then() + .catch(err => { log.error(err) }) } /** @@ -128,19 +134,11 @@ export class TestLoader implements vscode.Disposable { log.error('JSON parsing failed', error); } - let tests: Array = []; - - testMetadata.examples.forEach( - (test: ParsedTest) => { - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); // TODO: check this isn't RSpec specific - test.id = this.testSuite.normaliseTestId(test.id) - test.file_path = test.file_path.replace(this.config.getTestDirectory(), '') - tests.push(test); - log.debug("Parsed test", test) - } - ); + let tests = TestLoader.parseDryRunOutput( + this.log, + this.testSuite, + testMetadata.examples + ) log.debug("Test output parsed. Adding tests to test suite", tests) this.getTestSuiteForFile(tests, testItem); @@ -150,6 +148,41 @@ export class TestLoader implements vscode.Disposable { } } + public static parseDryRunOutput( + rootLog: IChildLogger, + testSuite: TestSuite, + tests: ParsedTest[] + ): ParsedTest[] { + let log = rootLog.getChildLogger({ label: "parseDryRunOutput" }) + log.debug(`called with ${tests.length} items`) + let parsedTests: Array = []; + + tests.forEach( + (test: ParsedTest) => { + log.debug("Parsing test", test) + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let test_location_string: string = test_location_array.join(''); + let location = parseInt(test_location_string); // TODO: check this isn't RSpec specific + let id = testSuite.normaliseTestId(test.id) + let file_path = TestLoader.normaliseFilePath(testSuite, test.file_path) + let parsedTest = { + ...test, + id: id, + file_path: file_path, + location: location, + } + parsedTests.push(parsedTest); + log.debug("Parsed test", parsedTest) + } + ); + return parsedTests + } + + public static normaliseFilePath(testSuite: TestSuite, filePath: string): string { + filePath = testSuite.normaliseTestId(filePath) + return filePath.replace(/\[.*/, '') + } + /** * Get the tests in a given file. * @@ -235,6 +268,8 @@ export class TestLoader implements vscode.Disposable { } private configWatcher(): vscode.Disposable { + let log = this.rootLog.getChildLogger({ label: "TestLoader.configWatcher" }); + log.debug('configWatcher') return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer")) { diff --git a/src/testSuite.ts b/src/testSuite.ts index 4c8ee05..26c8cf9 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -92,15 +92,21 @@ export class TestSuite { * - Removes leading test dir if present */ public normaliseTestId(testId: string): string { + let log = this.log.getChildLogger({label: `normaliseTestId(${testId})`}) if (testId.startsWith(`.${path.sep}`)) { + log.debug(`Stripping leading .${path.sep}`) testId = testId.substring(2) } + log.debug(`Checking if ID starts with test dir (${this.config.getTestDirectory()})`) if (testId.startsWith(this.config.getTestDirectory())) { + log.debug(`Stripping test dir (${this.config.getTestDirectory()})`) testId = testId.replace(this.config.getTestDirectory(), '') if (testId.startsWith(path.sep)) { + log.debug(`Stripping leading ${path.sep}`) testId = testId.substring(1) } } + log.debug(`Normalised ID: ${testId}`) return testId } diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 94b94f0..4136c8f 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -1,222 +1,92 @@ -// import { setupMockTestController, stdout_logger, testItemArrayMatches, testItemCollectionMatches } from "../helpers"; -// import { instance, mock, spy, when } from 'ts-mockito' -// import * as vscode from 'vscode' -// import * as path from 'path' -// import { expect } from "chai"; -// import { ParsedTest, TestLoader } from "../../../src/testLoader"; -// import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; -// import { RspecConfig } from "../../../src/rspec/rspecConfig"; -// import { TestSuite } from "../../../src/testSuite"; +import { expect } from "chai"; +import { before, beforeEach } from 'mocha'; +import { instance, mock, when } from 'ts-mockito' +import * as vscode from 'vscode' -// suite('TestLoader', function() { -// let mockTestController = setupMockTestController() -// let mockTestRunner = mock() -// let loader:TestLoader -// let testSuite: TestSuite -// let testController: vscode.TestController = instance(mockTestController) +import { Config } from "../../../src/config"; +import { ParsedTest, TestLoader } from "../../../src/testLoader"; +import { TestSuite } from "../../../src/testSuite"; +import { noop_logger, stdout_logger } from "../helpers"; +import { StubTestController } from '../../stubs/stubTestController'; -// const rspecDir = path.resolve("./test/fixtures/rspec/spec") -// const config = new RspecConfig(path.resolve("./ruby")) -// const configWrapper: RspecConfig = { -// frameworkName: config.frameworkName, -// getTestCommand: config.getTestCommand, -// getDebugCommand: config.getDebugCommand, -// getTestCommandWithFilePattern: config.getTestCommandWithFilePattern, -// getTestDirectory: () => rspecDir, -// getCustomFormatterLocation: config.getCustomFormatterLocation, -// testCommandWithFormatterAndDebugger: config.testCommandWithFormatterAndDebugger, -// getProcessEnv: config.getProcessEnv, -// rubyScriptPath: config.rubyScriptPath, -// getFilePattern: config.getFilePattern -// } +suite('TestLoader', function () { + let testSuite: TestSuite + let testController: vscode.TestController -// suite('#getBaseTestSuite()', function() { -// this.beforeEach(function() { -// let spiedWorkspace = spy(vscode.workspace) -// when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) -// .thenReturn({ get: (section: string) => { -// section == "framework" ? "rspec" : undefined -// }} as vscode.WorkspaceConfiguration) + const config = mock() -// testSuite = new TestSuite(stdout_logger(), testController, config) -// loader = new TestLoader(stdout_logger(), vscode.workspace.workspaceFolders![0], testController, instance(mockTestRunner), configWrapper, testSuite) -// let loaderSpy = spy(loader) -// }) + before(function () { + console.log('before') + when(config.getTestDirectory()).thenReturn('spec') + }) -// this.afterEach(function() { -// loader.dispose() -// }) + beforeEach(function () { + testController = new StubTestController() + testSuite = new TestSuite(stdout_logger(), testController, instance(config)) + }) -// test('single file with one test case', async function() { -// let tests: ParsedTest[] = [ -// { -// id: "abs_spec.rb[1:1]", -// full_description: "Abs finds the absolute value of 1", -// description: "finds the absolute value of 1", -// file_path: "abs_spec.rb", -// line_number: 4, -// location: 11, -// } -// ]; -// await loader.parseTestsInFile(vscode.Uri.file(path.resolve(rspecDir, "abs_spec.rb"))) + suite('#parseDryRunOutput()', function () { + const examples: ParsedTest[] = [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": null + } + ] + const parameters = examples.map( + (spec: ParsedTest, i: number, examples: ParsedTest[]) => { + return { + index: i, + spec: spec, + expected_location: i + 11, + expected_id: `abs_spec.rb[1:${i + 1}]`, + expected_file_path: 'abs_spec.rb' + } + } + ) -// expect(testController.items).to.have.property('size', 1) -// testItemCollectionMatches(testController.items, [ -// { -// id: "abs_spec.rb", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "abs_spec.rb", -// children: [ -// { -// id: "abs_spec.rb[1:1]", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "finds the absolute value of 1", -// line: 3 -// } -// ] -// } -// ]) -// }) - -// test('single file with two test cases', async function() { -// let tests: ParsedTest[] = [ -// { -// id: "abs_spec.rb[1:1]", -// full_description: "Abs finds the absolute value of 1", -// description: "finds the absolute value of 1", -// file_path: "abs_spec.rb", -// line_number: 4, -// location: 11, -// }, -// { -// id: "abs_spec.rb[1:2]", -// full_description: "Abs finds the absolute value of 0", -// description: "finds the absolute value of 0", -// file_path: "abs_spec.rb", -// line_number: 8, -// location: 12, -// } -// ]; -// let testItems: vscode.TestItem[] -// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - -// expect(testItems).to.not.be.undefined -// expect(testItems).to.have.length(1) -// testItemArrayMatches(testItems, [ -// { -// id: "abs_spec.rb", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "abs_spec.rb", -// children: [ -// { -// id: "abs_spec.rb[1:1]", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "finds the absolute value of 1", -// line: 3 -// }, -// { -// id: "abs_spec.rb[1:2]", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "finds the absolute value of 0", -// line: 7 -// }, -// ] -// } -// ]) -// }) - -// test('two files, one with a suite, each with one test case', async function() { -// let tests: ParsedTest[] = [ -// { -// id: "abs_spec.rb[1:1]", -// full_description: "Abs finds the absolute value of 1", -// description: "finds the absolute value of 1", -// file_path: "abs_spec.rb", -// line_number: 4, -// location: 11, -// }, -// { -// id: "square_spec.rb[1:1:1]", -// full_description: "Square an unnecessary suite finds the square of 2", -// description: "finds the square of 2", -// file_path: "square_spec.rb", -// line_number: 5, -// location: 111, -// } -// ]; -// let testItems: vscode.TestItem[] -// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - -// expect(testItems).to.not.be.undefined -// expect(testItems).to.have.length(2) -// testItemArrayMatches(testItems, [ -// { -// id: "abs_spec.rb", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "abs_spec.rb", -// children: [ -// { -// id: "abs_spec.rb[1:1]", -// file: path.join(rspecDir, "abs_spec.rb"), -// label: "finds the absolute value of 1", -// line: 3 -// } -// ] -// }, -// { -// id: "square_spec.rb", -// file: path.join(rspecDir, "square_spec.rb"), -// label: "square_spec.rb", -// children: [ -// { -// id: "square_spec.rb[1:1:1]", -// file: path.join(rspecDir, "square_spec.rb"), -// label: "an unnecessary suite finds the square of 2", -// line: 4 -// }, -// ] -// } -// ]) -// }) - -// test('subfolder containing single file with one test case', async function() { -// let tests: ParsedTest[] = [ -// { -// id: "subfolder/foo_spec.rb[1:1]", -// full_description: "Foo wibbles and wobbles", -// description: "wibbles and wobbles", -// file_path: "subfolder/foo_spec.rb", -// line_number: 3, -// location: 11, -// } -// ]; -// let testItems: vscode.TestItem[] -// expect(testItems = await loader["getBaseTestSuite"](tests)).to.not.throw - -// expect(testItems).to.not.be.undefined -// expect(testItems).to.have.length(1, 'Wrong number of children in controller.items') -// testItemArrayMatches(testItems, [ -// { -// id: "subfolder", -// file: path.join(rspecDir, "subfolder"), -// label: "subfolder", -// children: [ -// { -// id: "subfolder/foo_spec.rb", -// file: path.join(rspecDir, "subfolder", "foo_spec.rb"), -// label: "foo_spec.rb", -// children: [ -// { -// id: "subfolder/foo_spec.rb[1:1]", -// file: path.join(rspecDir, "subfolder", "foo_spec.rb"), -// label: "wibbles and wobbles", -// line: 2 -// } -// ] -// } -// ] -// } -// ]) -// }) -// }) -// }) \ No newline at end of file + parameters.forEach(({ + index, + spec, + expected_location, + expected_id, + expected_file_path + }) => { + test(`parses specs correctly - ${spec["id"]}`, function () { + let parsedSpec = TestLoader.parseDryRunOutput( + noop_logger(), + testSuite, + [spec] + )[0] + expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') + expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') + expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') + }) + }) + }) +}) diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index 623bc35..b9c03be 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -25,14 +25,15 @@ suite('TestSuite', function () { suite('#normaliseTestId()', function () { const parameters = [ - { arg: 'test-id', expected: 'test-id' }, - { arg: './test-id', expected: 'test-id' }, - { arg: 'folder/test-id', expected: 'folder/test-id' }, - { arg: './folder/test-id', expected: 'folder/test-id' }, - { arg: 'spec/test-id', expected: 'test-id' }, - { arg: './spec/test-id', expected: 'test-id' }, - { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, - { arg: './spec/folder/test-id', expected: 'folder/test-id' }, + { arg: 'test-id', expected: 'test-id' }, + { arg: './test-id', expected: 'test-id' }, + { arg: 'folder/test-id', expected: 'folder/test-id' }, + { arg: './folder/test-id', expected: 'folder/test-id' }, + { arg: 'spec/test-id', expected: 'test-id' }, + { arg: './spec/test-id', expected: 'test-id' }, + { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, ]; parameters.forEach(({ arg, expected }) => { From 028feee38a5de02a5c367195bd1b97c01adfc04f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 9 Nov 2022 21:53:33 +0000 Subject: [PATCH 035/108] Change Config.getTestDirectory() to getRelativeTestDirectory() and add getAbsoluteTestDirectory() --- src/config.ts | 14 ++++++++++-- src/minitest/minitestConfig.ts | 6 ++--- src/rspec/rspecConfig.ts | 6 ++--- src/rspec/rspecTestRunner.ts | 6 ++--- src/testFactory.ts | 1 - src/testLoader.ts | 5 +--- src/testRunner.ts | 11 +-------- src/testSuite.ts | 26 ++++++++++----------- test/fixtures/rspec/.vscode/settings.json | 3 ++- test/suite/helpers.ts | 16 ++++++++----- test/suite/rspec/rspec.test.ts | 20 +++++++++------- test/suite/unitTests/config.test.ts | 2 +- test/suite/unitTests/testLoader.test.ts | 2 +- test/suite/unitTests/testSuite.test.ts | 28 +++++++++++++++-------- 14 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/config.ts b/src/config.ts index ab89d81..891efaf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import * as childProcess from 'child_process'; import { IVSCodeExtLogger } from '@vscode-logging/logger'; @@ -30,11 +31,20 @@ export abstract class Config { } /** - * Get the user-configured test directory, if there is one. + * Get the user-configured test directory relative to the cwd, if there is one. * * @return The test directory */ - public abstract getTestDirectory(): string; + public abstract getRelativeTestDirectory(): string; + + /** + * Get the user-configured test directory relative to the cwd, if there is one. + * + * @return The test directory + */ + public getAbsoluteTestDirectory(): string { + return path.resolve(__dirname, this.getRelativeTestDirectory()) + } /** * Get the env vars to run the subprocess with. diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index 214e307..26497b9 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -12,7 +12,7 @@ export class MinitestConfig extends Config { * * @return The test directory */ - public getTestDirectory(): string { + public getRelativeTestDirectory(): string { return (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestDirectory') as string) || path.join('.', 'test'); } @@ -26,8 +26,8 @@ export class MinitestConfig extends Config { return Object.assign({}, process.env, { "RAILS_ENV": "test", "EXT_DIR": this.rubyScriptPath, - "TESTS_DIR": this.getTestDirectory(), + "TESTS_DIR": this.getRelativeTestDirectory(), "TESTS_PATTERN": this.getFilePattern().join(',') }); } -} \ No newline at end of file +} diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 145407f..5a23a44 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -42,7 +42,7 @@ export class RspecConfig extends Config { */ public getTestCommandWithFilePattern(): string { let command: string = this.getTestCommand() - const dir = this.getTestDirectory().replace(/\/$/, ""); + const dir = this.getRelativeTestDirectory().replace(/\/$/, ""); let pattern = this.getFilePattern().map(p => `${dir}/**/${p}`).join(',') return `${command} --pattern '${pattern}'`; } @@ -82,7 +82,7 @@ export class RspecConfig extends Config { }); } - public getTestDirectory(): string { + public getRelativeTestDirectory(): string { let configDir = vscode.workspace.getConfiguration('rubyTestExplorer').get('rspecDirectory') as string if (!configDir) return this.DEFAULT_TEST_DIRECTORY @@ -91,4 +91,4 @@ export class RspecConfig extends Config { configDir = configDir.substring(2) return configDir ?? this.DEFAULT_TEST_DIRECTORY; } -} \ No newline at end of file +} diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index f4b844e..dc764af 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -17,7 +17,7 @@ export class RspecTestRunner extends TestRunner { let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; testItems.forEach((item) => { - let testPath = `${cfg.getTestDirectory()}${path.sep}${item.id}` + let testPath = item.uri ? item.uri.fsPath : `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` cmd = `${cmd} ${testPath}` }) @@ -121,11 +121,11 @@ export class RspecTestRunner extends TestRunner { }; protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getTestDirectory()}${path.sep}${testItem.id}'` + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` }; protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getTestDirectory()}${path.sep}${testItem.id}'` + return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` }; protected getFullTestSuiteCommand(context: TestRunContext): string { diff --git a/src/testFactory.ts b/src/testFactory.ts index ec6a8d3..ccf6961 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -59,7 +59,6 @@ export class TestFactory implements vscode.Disposable { if (!this.loader) { this.loader = new TestLoader( this.log, - this.workspace, this.controller, this.getRunner(), this.config, diff --git a/src/testLoader.ts b/src/testLoader.ts index fc11a1d..98c6a92 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -32,16 +32,13 @@ export class TestLoader implements vscode.Disposable { constructor( readonly rootLog: IChildLogger, - private readonly workspace: vscode.WorkspaceFolder | undefined, private readonly controller: vscode.TestController, private readonly testRunner: RspecTestRunner | MinitestTestRunner, private readonly config: Config, private readonly testSuite: TestSuite, ) { this.log = rootLog.getChildLogger({ label: "TestLoader" }); - this.log.debug('constructor') this.disposables.push(this.configWatcher()); - this.log.debug('constructor complete') } dispose(): void { @@ -92,7 +89,7 @@ export class TestLoader implements vscode.Disposable { */ discoverAllFilesInWorkspace() { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) - let testDir = path.resolve(this.workspace?.uri.fsPath ?? '.', this.config.getTestDirectory()) + let testDir = this.config.getAbsoluteTestDirectory() log.debug(`testDir: ${testDir}`) let patterns: Array = [] diff --git a/src/testRunner.ts b/src/testRunner.ts index 2180c4c..46f4cfd 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; -import * as path from 'path' import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; @@ -138,15 +137,7 @@ export abstract class TestRunner implements vscode.Disposable { data = data.toString(); childProcessLogger.debug(data); let markTestStatus = (fn: (test: vscode.TestItem) => void, testId: string) => { - if (testId.startsWith(`.${path.sep}`)) { - testId = testId.substring(2) - } - if (testId.startsWith(this.config.getTestDirectory())) { - testId = testId.replace(this.config.getTestDirectory(), '') - if (testId.startsWith(path.sep)) { - testId = testId.substring(1) - } - } + testId = this.testSuite.normaliseTestId(testId) let test = this.testSuite.getOrCreateTestItem(testId) context.passed(test) } diff --git a/src/testSuite.ts b/src/testSuite.ts index 26c8cf9..2a249fc 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -61,7 +61,6 @@ export class TestSuite { collection, testId, label, - vscode.Uri.file(path.resolve(this.config.getTestDirectory(), testId.replace(this.locationPattern, ''))), !this.locationPattern.test(testId) ); } @@ -97,10 +96,10 @@ export class TestSuite { log.debug(`Stripping leading .${path.sep}`) testId = testId.substring(2) } - log.debug(`Checking if ID starts with test dir (${this.config.getTestDirectory()})`) - if (testId.startsWith(this.config.getTestDirectory())) { - log.debug(`Stripping test dir (${this.config.getTestDirectory()})`) - testId = testId.replace(this.config.getTestDirectory(), '') + log.debug(`Checking if ID starts with test dir (${this.config.getRelativeTestDirectory()})`) + if (testId.startsWith(this.config.getRelativeTestDirectory())) { + log.debug(`Stripping test dir (${this.config.getRelativeTestDirectory()})`) + testId = testId.replace(this.config.getRelativeTestDirectory(), '') if (testId.startsWith(path.sep)) { log.debug(`Stripping leading ${path.sep}`) testId = testId.substring(1) @@ -121,16 +120,17 @@ export class TestSuite { log.debug("uri is string. Returning unchanged") return uri } - let fullTestDirPath = path.resolve( - vscode.workspace?.workspaceFolders![0].uri.fsPath, - this.config.getTestDirectory() - ) + let fullTestDirPath = this.config.getAbsoluteTestDirectory() log.debug(`Full path to test dir: ${fullTestDirPath}`) let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') log.debug(`Stripped URI: ${strippedUri}`) return strippedUri } + private testIdToUri(testId: string): vscode.Uri { + return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId)) + } + /** * Searches the collection of tests for the TestItemCollection that contains the given test ID * @param testId ID of the test to get the parent collection of @@ -152,8 +152,7 @@ export class TestSuite { let child = this.createTestItem( collection, collectionId, - idSegments[i], - vscode.Uri.file(path.resolve(this.config.getTestDirectory(), collectionId)) + idSegments[i] ) childCollection = child.children } @@ -175,8 +174,7 @@ export class TestSuite { let child = this.createTestItem( collection, fileId, - fileId.substring(fileId.lastIndexOf(path.sep) + 1), - vscode.Uri.file(path.resolve(this.config.getTestDirectory(), fileId)) + fileId.substring(fileId.lastIndexOf(path.sep) + 1) ) childCollection = child.children } @@ -199,10 +197,10 @@ export class TestSuite { collection: vscode.TestItemCollection, testId: string, label: string, - uri: vscode.Uri, canResolveChildren: boolean = true ): vscode.TestItem { let log = this.log.getChildLogger({ label: `createTestId(${testId})` }) + let uri = this.testIdToUri(testId) log.debug(`Creating test item - label: ${label}, uri: ${uri}, canResolveChildren: ${canResolveChildren}`) let item = this.controller.createTestItem(testId, label, uri) item.canResolveChildren = canResolveChildren diff --git a/test/fixtures/rspec/.vscode/settings.json b/test/fixtures/rspec/.vscode/settings.json index 8806713..186f2fd 100644 --- a/test/fixtures/rspec/.vscode/settings.json +++ b/test/fixtures/rspec/.vscode/settings.json @@ -1,5 +1,6 @@ { "rubyTestExplorer": { "testFramework": "rspec" - } + }, + "rubyTestExplorer.rspecDirectory": "rspec/spec" } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index dc647a2..6d7b37d 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -87,6 +87,15 @@ export type TestItemExpectation = { children?: TestItemExpectation[] } +export function testUriMatches(testItem: vscode.TestItem, path?: string) { + if (path) { + expect(testItem.uri).to.not.be.undefined + expect(testItem.uri?.path).to.eql(path, `uri mismatch (id: ${testItem.id})`) + } else { + expect(testItem.uri).to.be.undefined + } +} + /** * Assert that a {@link vscode.TestItem TestItem} matches the expected values * @param testItem {@link vscode.TestItem TestItem} to check @@ -96,12 +105,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem if (!expectation) expect.fail("No expectation given") expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) - if (expectation.file) { - expect(testItem.uri).to.not.be.undefined - expect(testItem.uri?.path).to.eql(expectation.file, `uri mismatch (id: ${expectation.id})`) - } else { - expect(testItem.uri).to.be.undefined - } + testUriMatches(testItem, expectation.file) if (expectation.children && expectation.children.length > 0) { expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) let i = 0; diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 8803333..3ba8655 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -3,12 +3,13 @@ import * as path from 'path' import { anything, instance, verify } from 'ts-mockito' import { expect } from 'chai'; -import { RspecTestRunner } from 'src/rspec/rspecTestRunner'; -import { TestLoader } from 'src/testLoader'; -import { RspecConfig } from 'src/rspec/rspecConfig'; -import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from 'test/suite/helpers'; -import { StubTestController } from 'test/stubs/stubTestController'; -import { TestSuite } from 'src/testSuite'; +import { TestLoader } from '../../../src/testLoader'; +import { TestSuite } from '../../../src/testSuite'; +import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; +import { RspecConfig } from '../../../src/rspec/rspecConfig'; + +import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -29,7 +30,8 @@ suite('Extension Test for RSpec', function() { file) } - this.beforeEach(async function() { + this.beforeEach(async function () { + vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'rspec/spec') testController = new StubTestController() // Populate controller with test files. This would be done by the filesystem globs in the watchers @@ -41,9 +43,11 @@ suite('Extension Test for RSpec', function() { subfolderItem.children.add(createTest("subfolder/foo_spec.rb")) config = new RspecConfig(dirPath) + console.log(`dirpath: ${dirPath}`) + let testSuite = new TestSuite(stdout_logger(), testController, config) testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(stdout_logger(), workspaceFolder, testController, testRunner, config, testSuite); + testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); }) test('Load tests on file resolve request', async function () { diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index dcfee43..7d0dcb2 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -62,7 +62,7 @@ suite('Config', function() { suite("#getTestDirectory()", function() { test("with no config set, it returns default value", function() { let config = new RspecConfig("../../../ruby") - expect(config.getTestDirectory()).to.eq("spec/") + expect(config.getRelativeTestDirectory()).to.eq("spec/") }) }) }) diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 4136c8f..4a684fd 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -17,7 +17,7 @@ suite('TestLoader', function () { before(function () { console.log('before') - when(config.getTestDirectory()).thenReturn('spec') + when(config.getRelativeTestDirectory()).thenReturn('spec') }) beforeEach(function () { diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index b9c03be..d7e2bcd 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -2,25 +2,28 @@ import { expect } from 'chai'; import { before, beforeEach } from 'mocha'; import { instance, mock, when } from 'ts-mockito' import * as vscode from 'vscode' +import path from 'path' import { Config } from '../../../src/config'; import { TestSuite } from '../../../src/testSuite'; import { StubTestController } from '../../stubs/stubTestController'; import { StubTestItem } from '../../stubs/stubTestItem'; -import { noop_logger } from '../helpers'; +import { noop_logger, testUriMatches } from '../helpers'; suite('TestSuite', function () { - let config: Config = mock(); + let mockConfig: Config = mock(); + const config: Config = instance(mockConfig) let controller: vscode.TestController; let testSuite: TestSuite; before(function () { - when(config.getTestDirectory()).thenReturn('spec') + when(mockConfig.getRelativeTestDirectory()).thenReturn('spec') + when(mockConfig.getAbsoluteTestDirectory()).thenReturn(path.resolve('spec')) }); beforeEach(function () { controller = new StubTestController() - testSuite = new TestSuite(noop_logger(), controller, instance(config)) + testSuite = new TestSuite(noop_logger(), controller, instance(mockConfig)) }); suite('#normaliseTestId()', function () { @@ -110,7 +113,7 @@ suite('TestSuite', function () { const id = 'test-id' const label = 'test-label' const testItem = new StubTestItem(id, label) - const childId = 'folder/child-test' + const childId = `folder${path.sep}child-test` const childItem = new StubTestItem(childId, 'child-test') test('gets the specified item if ID is found', function () { @@ -122,6 +125,7 @@ suite('TestSuite', function () { let testItem = testSuite.getOrCreateTestItem('not-found') expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq('not-found') + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), 'not-found')) }) test('gets the specified nested test if ID is found', function () { @@ -134,22 +138,26 @@ suite('TestSuite', function () { }) test('creates item if nested ID is not found', function () { + let id = `folder${path.sep}not-found` let folderItem = new StubTestItem('folder', 'folder') controller.items.add(folderItem) - let testItem = testSuite.getOrCreateTestItem('folder/not-found') + let testItem = testSuite.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined - expect(testItem?.id).to.eq('folder/not-found') + expect(testItem?.id).to.eq(id) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) }) test('creates item and parent if parent of nested ID is not found', function () { - let testItem = testSuite.getOrCreateTestItem('folder/not-found') + let id = `folder${path.sep}not-found` + let testItem = testSuite.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined - expect(testItem?.id).to.eq('folder/not-found') + expect(testItem?.id).to.eq(id) let folder = testSuite.getOrCreateTestItem('folder') expect(folder?.children.size).to.eq(1) - expect(folder?.children.get('folder/not-found')).to.eq(testItem) + expect(folder?.children.get(id)).to.eq(testItem) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) }) }) }); From 0f2719fb27b7681d135a047601f8c42b8c959aac Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 12 Nov 2022 16:46:22 +0000 Subject: [PATCH 036/108] Fix getAbsoluteTestFolder --- src/config.ts | 12 +++++-- src/rspec/rspecConfig.ts | 3 +- test/fixtures/rspec/.vscode/settings.json | 2 +- test/runFrameworkTests.ts | 5 +-- test/suite/rspec/rspec.test.ts | 10 +++--- test/suite/unitTests/config.test.ts | 40 ++++++++++++++--------- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/config.ts b/src/config.ts index 891efaf..4117711 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,8 +5,15 @@ import { IVSCodeExtLogger } from '@vscode-logging/logger'; export abstract class Config { + /** + * Full path to the ruby script directory + */ public readonly rubyScriptPath: 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) + */ constructor(context: vscode.ExtensionContext | string) { if (typeof context === "object") { this.rubyScriptPath = vscode.Uri.joinPath(context?.extensionUri ?? vscode.Uri.file("./"), 'ruby').fsPath; @@ -26,7 +33,8 @@ export abstract class Config { * @return The file pattern */ public getFilePattern(): Array { - let pattern: Array = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array); + let pattern: Array = + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('filePattern') as Array; return pattern || ['*_test.rb', 'test_*.rb']; } @@ -43,7 +51,7 @@ export abstract class Config { * @return The test directory */ public getAbsoluteTestDirectory(): string { - return path.resolve(__dirname, this.getRelativeTestDirectory()) + return path.resolve(this.getRelativeTestDirectory()) } /** diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 5a23a44..6aac1fe 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -15,7 +15,8 @@ export class RspecConfig extends Config { * @return The RSpec command */ public getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string); + let command: string = + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('rspecCommand') as string; return command || `bundle exec rspec` } diff --git a/test/fixtures/rspec/.vscode/settings.json b/test/fixtures/rspec/.vscode/settings.json index 186f2fd..b384fad 100644 --- a/test/fixtures/rspec/.vscode/settings.json +++ b/test/fixtures/rspec/.vscode/settings.json @@ -2,5 +2,5 @@ "rubyTestExplorer": { "testFramework": "rspec" }, - "rubyTestExplorer.rspecDirectory": "rspec/spec" + "rubyTestExplorer.rspecDirectory": "test/fixtures/rspec/spec" } diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 8f50a28..684124f 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -35,8 +35,9 @@ async function runTestSuite(vscodeExecutablePath: string, suite: string) { let testsPath = path.resolve(__dirname, `suite`) let fixturesPath = path.resolve(extensionDevelopmentPath, `test/fixtures/${suite}`) - console.debug(`testsPath: ${testsPath}`) - console.debug(`fixturesPath: ${fixturesPath}`) + console.debug(`extensionDevelopmentPath: ${extensionDevelopmentPath}`) // Root folder of repository + console.debug(`testsPath: ${testsPath}`) // Path to tests/suite + console.debug(`fixturesPath: ${fixturesPath}`) // Workspace folder await runTests({ extensionDevelopmentPath, diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 3ba8655..3aaa89d 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -18,11 +18,8 @@ suite('Extension Test for RSpec', function() { let testRunner: RspecTestRunner; let testLoader: TestLoader; - const dirPath = path.resolve("ruby") let expectedPath = (file: string): string => { return path.resolve( - dirPath, - '..', 'test', 'fixtures', 'rspec', @@ -31,7 +28,7 @@ suite('Extension Test for RSpec', function() { } this.beforeEach(async function () { - vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'rspec/spec') + vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'test/fixtures/rspec/spec') testController = new StubTestController() // Populate controller with test files. This would be done by the filesystem globs in the watchers @@ -42,8 +39,9 @@ suite('Extension Test for RSpec', function() { testController.items.add(subfolderItem) subfolderItem.children.add(createTest("subfolder/foo_spec.rb")) - config = new RspecConfig(dirPath) - console.log(`dirpath: ${dirPath}`) + config = new RspecConfig(path.resolve("ruby")) + console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) + console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) let testSuite = new TestSuite(stdout_logger(), testController, config) testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index 7d0dcb2..a5e3a08 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { spy, when } from 'ts-mockito' import * as vscode from 'vscode' +import * as path from 'path' import { Config } from "../../../src/config"; import { RspecConfig } from "../../../src/rspec/rspecConfig"; @@ -37,33 +38,40 @@ suite('Config', function() { }); suite("Rspec specific tests", function() { - test("#getTestCommandWithFilePattern", function() { - const spiedWorkspace = spy(vscode.workspace) - const configSection: { get(section: string): any | undefined } | undefined = { - get: (section: string) => { - switch (section) { - case "framework": - return "rspec" - case "filePattern": - return ['*_test.rb', 'test_*.rb'] - default: - return undefined - } + const configSection: { get(section: string): any | undefined } | undefined = { + get: (section: string) => { + switch (section) { + case "framework": + return "rspec" + case "filePattern": + return ['*_test.rb', 'test_*.rb'] + default: + return undefined } } + } + + test("#getTestCommandWithFilePattern", function() { + let spiedWorkspace = spy(vscode.workspace) when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) .thenReturn(configSection as vscode.WorkspaceConfiguration) - - let config = new RspecConfig("../../../ruby") + let config = new RspecConfig(path.resolve('ruby')) expect(config.getTestCommandWithFilePattern()).to .eq("bundle exec rspec --pattern 'spec/**/*_test.rb,spec/**/test_*.rb'") }) - suite("#getTestDirectory()", function() { + suite("#getRelativeTestDirectory()", function() { test("with no config set, it returns default value", function() { - let config = new RspecConfig("../../../ruby") + let config = new RspecConfig(path.resolve('ruby')) expect(config.getRelativeTestDirectory()).to.eq("spec/") }) }) + + suite('#getAbsoluteTestDirectory()', function () { + test('returns path to workspace with relative path appended', function () { + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getAbsoluteTestDirectory()).to.eq(path.resolve('spec')) + }) + }) }) }); From 72b5b0325c3e6023ff3fcae93e0fa1b3bfb06762 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 12 Nov 2022 17:13:58 +0000 Subject: [PATCH 037/108] Fix normaliseTestId to also strip root test folder --- src/rspec/rspecTestRunner.ts | 4 ++-- src/testSuite.ts | 14 +++++++++---- test/suite/unitTests/testSuite.test.ts | 28 ++++++++++++++++---------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index dc764af..fbb5ddd 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -23,11 +23,11 @@ export class RspecTestRunner extends TestRunner { this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); this.log.debug(`cwd: ${__dirname}`) - this.log.debug(`child process cwd: ${this.workspace?.uri.fsPath}`) + this.log.debug(`child process cwd: ${this.config.getAbsoluteTestDirectory()}`) // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { - cwd: this.workspace?.uri.fsPath, + cwd: path.resolve(this.config.getAbsoluteTestDirectory(), '..'), maxBuffer: 8192 * 8192, }; diff --git a/src/testSuite.ts b/src/testSuite.ts index 2a249fc..735c366 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -100,10 +100,16 @@ export class TestSuite { if (testId.startsWith(this.config.getRelativeTestDirectory())) { log.debug(`Stripping test dir (${this.config.getRelativeTestDirectory()})`) testId = testId.replace(this.config.getRelativeTestDirectory(), '') - if (testId.startsWith(path.sep)) { - log.debug(`Stripping leading ${path.sep}`) - testId = testId.substring(1) - } + } + let testDirParent = this.config.getRelativeTestDirectory().split(path.sep).pop() + log.debug(`Checking if ID starts with test root (${testDirParent})`) + if (testDirParent && testId.startsWith(testDirParent)) { + log.debug(`Stripping test root dir (${testDirParent})`) + testId = testId.replace(testDirParent, '') + } + if (testId.startsWith(path.sep)) { + log.debug(`Stripping leading ${path.sep}`) + testId = testId.substring(1) } log.debug(`Normalised ID: ${testId}`) return testId diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index d7e2bcd..fca7d30 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -17,8 +17,9 @@ suite('TestSuite', function () { let testSuite: TestSuite; before(function () { - when(mockConfig.getRelativeTestDirectory()).thenReturn('spec') - when(mockConfig.getAbsoluteTestDirectory()).thenReturn(path.resolve('spec')) + let relativeTestPath = 'path/to/spec' + when(mockConfig.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(mockConfig.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) }); beforeEach(function () { @@ -28,15 +29,20 @@ suite('TestSuite', function () { suite('#normaliseTestId()', function () { const parameters = [ - { arg: 'test-id', expected: 'test-id' }, - { arg: './test-id', expected: 'test-id' }, - { arg: 'folder/test-id', expected: 'folder/test-id' }, - { arg: './folder/test-id', expected: 'folder/test-id' }, - { arg: 'spec/test-id', expected: 'test-id' }, - { arg: './spec/test-id', expected: 'test-id' }, - { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, - { arg: './spec/folder/test-id', expected: 'folder/test-id' }, - { arg: './spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, + { arg: 'test-id', expected: 'test-id' }, + { arg: './test-id', expected: 'test-id' }, + { arg: 'folder/test-id', expected: 'folder/test-id' }, + { arg: './folder/test-id', expected: 'folder/test-id' }, + { arg: 'spec/test-id', expected: 'test-id' }, + { arg: './spec/test-id', expected: 'test-id' }, + { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, + { arg: 'path/to/spec/test-id', expected: 'test-id' }, + { arg: './path/to/spec/test-id', expected: 'test-id' }, + { arg: 'path/to/spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './path/to/spec/folder/test-id', expected: 'folder/test-id' }, + { arg: './path/to/spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, ]; parameters.forEach(({ arg, expected }) => { From 7df3b9b68c69bb7768d75409cec0bc5bf262c66d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 12 Nov 2022 19:00:26 +0000 Subject: [PATCH 038/108] Get RSpec test suite passing --- src/config.ts | 15 +-- src/main.ts | 18 +++- src/rspec/rspecTestRunner.ts | 8 +- src/testLoader.ts | 1 + src/testSuite.ts | 12 +-- test/fixtures/rspec/.vscode/settings.json | 2 +- test/suite/helpers.ts | 5 +- test/suite/rspec/rspec.test.ts | 107 ++++++++++++---------- test/suite/unitTests/testSuite.test.ts | 5 - 9 files changed, 92 insertions(+), 81 deletions(-) diff --git a/src/config.ts b/src/config.ts index 4117711..7773d6f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,16 +10,19 @@ export abstract class Config { */ public readonly rubyScriptPath: string; + public readonly workspaceFolder?: vscode.WorkspaceFolder; + /** * @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) */ - constructor(context: vscode.ExtensionContext | string) { + constructor(context: vscode.ExtensionContext | string, workspaceFolder?: vscode.WorkspaceFolder) { if (typeof context === "object") { this.rubyScriptPath = vscode.Uri.joinPath(context?.extensionUri ?? vscode.Uri.file("./"), 'ruby').fsPath; } else { this.rubyScriptPath = (context as string) } + this.workspaceFolder = workspaceFolder } /** @@ -39,20 +42,20 @@ export abstract class Config { } /** - * Get the user-configured test directory relative to the cwd, if there is one. + * Get the user-configured test directory relative to the test project root folder, if there is one. * * @return The test directory */ public abstract getRelativeTestDirectory(): string; /** - * Get the user-configured test directory relative to the cwd, if there is one. + * Get the absolute path to user-configured test directory, if there is one. * * @return The test directory */ - public getAbsoluteTestDirectory(): string { - return path.resolve(this.getRelativeTestDirectory()) - } + public getAbsoluteTestDirectory(): string { + return path.resolve(this.workspaceFolder?.uri.fsPath || '.', this.getRelativeTestDirectory()) + } /** * Get the env vars to run the subprocess with. diff --git a/src/main.ts b/src/main.ts index df8312d..5ceee3f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,23 @@ import * as vscode from 'vscode'; -import { getExtensionLogger } from "@vscode-logging/logger"; +import { getExtensionLogger, IChildLogger } from "@vscode-logging/logger"; import { TestFactory } from './testFactory'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; import { Config } from './config'; -export const guessWorkspaceFolder = async () => { +export const guessWorkspaceFolder = async (rootLog: IChildLogger) => { + let log = rootLog.getChildLogger({ label: "guessWorkspaceFolder: " }) if (!vscode.workspace.workspaceFolders) { return undefined; } + console.debug("Found workspace folders:") + log.debug("Found workspace folders:") + for (const folder of vscode.workspace.workspaceFolders) { + console.debug(` - ${folder.uri.fsPath}`) + log.debug(` - ${folder.uri.fsPath}`) + } + if (vscode.workspace.workspaceFolders.length < 2) { return vscode.workspace.workspaceFolders[0]; } @@ -41,12 +49,12 @@ export async function activate(context: vscode.ExtensionContext) { log.error("No workspace opened") } - const workspace: vscode.WorkspaceFolder | undefined = await guessWorkspaceFolder(); + const workspace: vscode.WorkspaceFolder | undefined = await guessWorkspaceFolder(log); let testFramework: string = Config.getTestFramework(log); let testConfig = testFramework == "rspec" - ? new RspecConfig(context) - : new MinitestConfig(context) + ? new RspecConfig(context, workspace) + : new MinitestConfig(context, workspace) const debuggerConfig: vscode.DebugConfiguration = { name: "Debug Ruby Tests", diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index fbb5ddd..41fe560 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -17,17 +17,17 @@ export class RspecTestRunner extends TestRunner { let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; testItems.forEach((item) => { - let testPath = item.uri ? item.uri.fsPath : `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` - cmd = `${cmd} ${testPath}` + let testPath = `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` + cmd = `${cmd} "${testPath}"` }) this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); this.log.debug(`cwd: ${__dirname}`) - this.log.debug(`child process cwd: ${this.config.getAbsoluteTestDirectory()}`) + this.log.debug(`child process cwd: ${this.workspace?.uri.fsPath}`) // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { - cwd: path.resolve(this.config.getAbsoluteTestDirectory(), '..'), + cwd: this.workspace?.uri.fsPath, maxBuffer: 8192 * 8192, }; diff --git a/src/testLoader.ts b/src/testLoader.ts index 98c6a92..78a9812 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -224,6 +224,7 @@ export class TestLoader implements vscode.Disposable { let childTestItem = this.testSuite.getOrCreateTestItem(test.id) childTestItem.canResolveChildren = false + log.debug(`Setting test ${childTestItem.id} label to "${description}"`) childTestItem.label = description childTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); diff --git a/src/testSuite.ts b/src/testSuite.ts index 735c366..90be246 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -93,22 +93,12 @@ export class TestSuite { public normaliseTestId(testId: string): string { let log = this.log.getChildLogger({label: `normaliseTestId(${testId})`}) if (testId.startsWith(`.${path.sep}`)) { - log.debug(`Stripping leading .${path.sep}`) testId = testId.substring(2) } - log.debug(`Checking if ID starts with test dir (${this.config.getRelativeTestDirectory()})`) if (testId.startsWith(this.config.getRelativeTestDirectory())) { - log.debug(`Stripping test dir (${this.config.getRelativeTestDirectory()})`) testId = testId.replace(this.config.getRelativeTestDirectory(), '') } - let testDirParent = this.config.getRelativeTestDirectory().split(path.sep).pop() - log.debug(`Checking if ID starts with test root (${testDirParent})`) - if (testDirParent && testId.startsWith(testDirParent)) { - log.debug(`Stripping test root dir (${testDirParent})`) - testId = testId.replace(testDirParent, '') - } if (testId.startsWith(path.sep)) { - log.debug(`Stripping leading ${path.sep}`) testId = testId.substring(1) } log.debug(`Normalised ID: ${testId}`) @@ -134,7 +124,7 @@ export class TestSuite { } private testIdToUri(testId: string): vscode.Uri { - return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId)) + return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId.replace(/\[.*\]/, ''))) } /** diff --git a/test/fixtures/rspec/.vscode/settings.json b/test/fixtures/rspec/.vscode/settings.json index b384fad..277195e 100644 --- a/test/fixtures/rspec/.vscode/settings.json +++ b/test/fixtures/rspec/.vscode/settings.json @@ -2,5 +2,5 @@ "rubyTestExplorer": { "testFramework": "rspec" }, - "rubyTestExplorer.rspecDirectory": "test/fixtures/rspec/spec" + "rubyTestExplorer.rspecDirectory": "spec" } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 6d7b37d..683a4a6 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -5,6 +5,7 @@ import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; +import { TestSuite } from '../../src/testSuite'; export function noop() {} @@ -176,10 +177,10 @@ export function setupMockTestController(): vscode.TestController { return mockTestController } -export function setupMockRequest(testController: vscode.TestController, testId?: string): vscode.TestRunRequest { +export function setupMockRequest(testSuite: TestSuite, testId?: string): vscode.TestRunRequest { let mockRequest = mock() if (testId) { - let testItem = testController.items.get(testId) + let testItem = testSuite.getTestItem(testId) if (testItem === undefined) { throw new Error("Couldn't find test") } diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 3aaa89d..91d4890 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -8,7 +8,7 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { noop_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -17,6 +17,7 @@ suite('Extension Test for RSpec', function() { let config: RspecConfig let testRunner: RspecTestRunner; let testLoader: TestLoader; + let testSuite: TestSuite; let expectedPath = (file: string): string => { return path.resolve( @@ -27,25 +28,38 @@ suite('Extension Test for RSpec', function() { file) } + this.beforeAll(function () { + if (vscode.workspace.workspaceFolders) { + console.debug("Found workspace folders (test):") + for (const folder of vscode.workspace.workspaceFolders) { + console.debug(` - ${folder.uri.fsPath}`) + } + } else { + console.debug("No workspace folders open") + } + }) + this.beforeEach(async function () { - vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'test/fixtures/rspec/spec') + vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') testController = new StubTestController() // Populate controller with test files. This would be done by the filesystem globs in the watchers - let createTest = (id: string) => testController.createTestItem(id, id, vscode.Uri.file(expectedPath(id))) + let createTest = (id: string, label?: string) => + testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) testController.items.add(createTest("abs_spec.rb")) testController.items.add(createTest("square_spec.rb")) let subfolderItem = createTest("subfolder") testController.items.add(subfolderItem) - subfolderItem.children.add(createTest("subfolder/foo_spec.rb")) + subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) - config = new RspecConfig(path.resolve("ruby")) + console.debug(`Workspace folder used in test: ${workspaceFolder.uri.fsPath}`) + config = new RspecConfig(path.resolve("ruby"), workspaceFolder) console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) - let testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -216,7 +230,7 @@ suite('Extension Test for RSpec', function() { test('run test success', async function() { await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) - let mockRequest = setupMockRequest(testController, "square_spec.rb") + let mockRequest = setupMockRequest(testSuite, "square_spec.rb") let request = instance(mockRequest) let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) @@ -230,30 +244,35 @@ suite('Extension Test for RSpec', function() { label: "finds the square of 2", line: 3 } + + // Passed called once per test in file during dry run testItemMatches(args.passedArg(0)["testItem"], expectation) - testItemMatches(args.passedArg(1)["testItem"], expectation) + testItemMatches( + args.passedArg(1)["testItem"], + { + id: "square_spec.rb[1:2]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 3", + line: 7 + } + ) + + // Passed called again for passing test but not for failing test testItemMatches(args.passedArg(2)["testItem"], expectation) - testItemMatches(args.passedArg(3)["testItem"], expectation) - verify(mockTestRun.passed(anything(), undefined)).times(4) + verify(mockTestRun.passed(anything(), undefined)).times(3) }) test('run test failure', async function() { await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) - let mockRequest = setupMockRequest(testController, "square_spec.rb") + let mockRequest = setupMockRequest(testSuite, "square_spec.rb") let request = instance(mockRequest) let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) let mockTestRun = (testController as StubTestController).getMockTestRun() - let args = testStateCaptors(mockTestRun) - - // Initial failure event with no details - let firstCallArgs = args.failedArg(0) - expect(firstCallArgs.testItem).to.have.property("id", "square_spec.rb[1:2]") - expect(firstCallArgs.testItem).to.have.property("label", "finds the square of 3") - expect(firstCallArgs.message.location?.uri.fsPath).to.eq(expectedPath("square_spec.rb")) + let args = testStateCaptors(mockTestRun).failedArg(0) // Actual failure report let expectation = { @@ -262,36 +281,30 @@ suite('Extension Test for RSpec', function() { label: "finds the square of 3", line: 7 } - let failedArg = args.failedArg(1) - testItemMatches(failedArg["testItem"], expectation) + testItemMatches(args.testItem, expectation) - expect(failedArg["message"].message).to.contain("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n") - expect(failedArg["message"].actualOutput).to.be.undefined - expect(failedArg["message"].expectedOutput).to.be.undefined - expect(failedArg["message"].location?.range.start.line).to.eq(8) - expect(failedArg["message"].location?.uri.fsPath).to.eq(expectation.file) + expect(args.message.message).to.contain("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n") + expect(args.message.actualOutput).to.be.undefined + expect(args.message.expectedOutput).to.be.undefined + expect(args.message.location?.range.start.line).to.eq(8) + expect(args.message.location?.uri.fsPath).to.eq(expectation.file) + expect(args.message.location?.uri.fsPath).to.eq(expectedPath("square_spec.rb")) - verify(mockTestRun.started(anything())).times(3) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(4) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) }) test('run test error', async function() { await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:2]") + let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:2]") let request = instance(mockRequest) let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) let mockTestRun = (testController as StubTestController).getMockTestRun() - let args = testStateCaptors(mockTestRun) - - // Initial failure event with no details - let firstCallArgs = args.failedArg(0) - expect(firstCallArgs.testItem).to.have.property("id", "abs_spec.rb[1:2]") - expect(firstCallArgs.testItem).to.have.property("label", "finds the absolute value of 0") - expect(firstCallArgs.message.location?.uri.fsPath).to.eq(expectedPath("abs_spec.rb")) + let args = testStateCaptors(mockTestRun).failedArg(0) // Actual failure report let expectation = { @@ -300,22 +313,22 @@ suite('Extension Test for RSpec', function() { label: "finds the absolute value of 0", line: 7, } - let failedArg = args.failedArg(1) - testItemMatches(failedArg["testItem"], expectation) - - expect(failedArg["message"].message).to.match(/RuntimeError:\nAbs for zero is not supported/) - expect(failedArg["message"].actualOutput).to.be.undefined - expect(failedArg["message"].expectedOutput).to.be.undefined - expect(failedArg["message"].location?.range.start.line).to.eq(8) - expect(failedArg["message"].location?.uri.fsPath).to.eq(expectation.file) + testItemMatches(args.testItem, expectation) + + expect(args.message.message).to.match(/RuntimeError:\nAbs for zero is not supported/) + expect(args.message.actualOutput).to.be.undefined + expect(args.message.expectedOutput).to.be.undefined + expect(args.message.location?.range.start.line).to.eq(8) + expect(args.message.location?.uri.fsPath).to.eq(expectation.file) + expect(args.message.location?.uri.fsPath).to.eq(expectedPath("abs_spec.rb")) verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(2) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) }) test('run test skip', async function() { await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - let mockRequest = setupMockRequest(testController, "abs_spec.rb[1:3]") + let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:3]") let request = instance(mockRequest) let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index fca7d30..e47e164 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -33,11 +33,6 @@ suite('TestSuite', function () { { arg: './test-id', expected: 'test-id' }, { arg: 'folder/test-id', expected: 'folder/test-id' }, { arg: './folder/test-id', expected: 'folder/test-id' }, - { arg: 'spec/test-id', expected: 'test-id' }, - { arg: './spec/test-id', expected: 'test-id' }, - { arg: 'spec/folder/test-id', expected: 'folder/test-id' }, - { arg: './spec/folder/test-id', expected: 'folder/test-id' }, - { arg: './spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, { arg: 'path/to/spec/test-id', expected: 'test-id' }, { arg: './path/to/spec/test-id', expected: 'test-id' }, { arg: 'path/to/spec/folder/test-id', expected: 'folder/test-id' }, From 2fc6539f87eaa423ddea3c1a761ccdec3c2a34bc Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 27 Nov 2022 17:01:19 +0000 Subject: [PATCH 039/108] Get minitest working (Kind of hacky for now) --- src/minitest/minitestConfig.ts | 40 ++ src/minitest/minitestTestRunner.ts | 87 +--- src/testFactory.ts | 4 +- src/testLoader.ts | 12 +- test/fixtures/minitest/.vscode/settings.json | 3 +- .../minitest/test/{ => square}/square_test.rb | 2 +- test/stubs/stubTestItemCollection.ts | 11 +- test/suite/helpers.ts | 20 +- test/suite/minitest/minitest.test.ts | 457 +++++++++++------- test/suite/rspec/rspec.test.ts | 3 +- test/suite/unitTests/testLoader.test.ts | 236 ++++++--- test/suite/unitTests/testSuite.test.ts | 1 + 12 files changed, 545 insertions(+), 331 deletions(-) rename test/fixtures/minitest/test/{ => square}/square_test.rb (85%) diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index 26497b9..9c111a9 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -7,6 +7,46 @@ export class MinitestConfig extends Config { return "Minitest" } + /** + * Get the user-configured Minitest command, if there is one. + * + * @return The Minitest command + */ + public getTestCommand(): string { + let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; + return `${command} -R ${this.rubyScriptPath}`; + } + + /** + * Get the user-configured rdebug-ide command, if there is one. + * + * @return The rdebug-ide command + */ + public getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { + let command: string = + (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || + 'rdebug-ide'; + + return ( + `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + + ` -- ${this.rubyScriptPath}/debug_minitest.rb` + ); + } + + /** + * Get test command with formatter and debugger arguments + * + * @param debuggerConfig A VS Code debugger configuration. + * @return The test command + */ + public testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + let cmd = `${this.getTestCommand()} vscode:minitest:run` + if (debuggerConfig) { + cmd = this.getDebugCommand(debuggerConfig); + } + return cmd; + } + /** * Get the user-configured test directory, if there is one. * diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index ad0f47f..35b540f 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; +import * as path from 'path' import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; +import { MinitestConfig } from './minitestConfig'; export class MinitestTestRunner extends TestRunner { testFrameworkName = 'Minitest'; @@ -12,7 +14,7 @@ export class MinitestTestRunner extends TestRunner { * @return The raw output from the Minitest JSON formatter. */ initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { - let cmd = `${this.getTestCommand()} vscode:minitest:list`; + let cmd = `${(this.config as MinitestConfig).getTestCommand()} vscode:minitest:list`; // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { @@ -21,10 +23,6 @@ export class MinitestTestRunner extends TestRunner { env: this.config.getProcessEnv() }; - testItems.forEach((item) => { - cmd = `${cmd} ${item.id}` - }) - this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); childProcess.exec(cmd, execArgs, (err, stdout) => { @@ -40,59 +38,19 @@ export class MinitestTestRunner extends TestRunner { }); }); - /** - * Get the user-configured Minitest command, if there is one. - * - * @return The Minitest command - */ - protected getTestCommand(): string { - let command: string = (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('minitestCommand') as string) || 'bundle exec rake'; - return `${command} -R ${(process.platform == 'win32') ? '%EXT_DIR%' : '$EXT_DIR'}`; - } - - /** - * Get the user-configured rdebug-ide command, if there is one. - * - * @return The rdebug-ide command - */ - protected getDebugCommand(debuggerConfig: vscode.DebugConfiguration): string { - let command: string = - (vscode.workspace.getConfiguration('rubyTestExplorer', null).get('debugCommand') as string) || - 'rdebug-ide'; - - return ( - `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + - ` -- ${process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR'}/debug_minitest.rb` - ); - } - - /** - * Get test command with formatter and debugger arguments - * - * @param debuggerConfig A VS Code debugger configuration. - * @return The test command - */ - protected testCommandWithDebugger(debuggerConfig?: vscode.DebugConfiguration): string { - let cmd = `${this.getTestCommand()} vscode:minitest:run` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig); - } - return cmd; - } - protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { let line = testItem.id.split(':').pop(); - let relativeLocation = testItem.id.split(/:\d+$/)[0].replace(`${this.workspace?.uri.fsPath || "."}/`, "") - return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeLocation}:${line}'` + let relativeLocation = `${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}` + return `${(this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig)} '${relativeLocation}:${line}'` }; protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { - let relativeFile = testItem.id.replace(`${this.workspace?.uri.fsPath || '.'}/`, "").replace(`./`, "") - return `${this.testCommandWithDebugger(context.debuggerConfig)} '${relativeFile}'` + let relativeFile = `${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}` + return `${(this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig)} '${relativeFile}'` }; protected getFullTestSuiteCommand(context: TestRunContext): string { - return this.testCommandWithDebugger(context.debuggerConfig) + return (this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig) }; /** @@ -107,6 +65,7 @@ export class MinitestTestRunner extends TestRunner { if (test.status === "passed") { context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { + // Failed/Errored let errorMessageLine: number = test.line_number; let errorMessage: string = test.exception.message; @@ -126,20 +85,24 @@ export class MinitestTestRunner extends TestRunner { }); } - context.failed( - testItem, - errorMessage, - test.file_path.replace('./', ''), - errorMessageLine - 1 - ) + if (test.exception.class === "Minitest::UnexpectedError") { + context.errored( + testItem, + errorMessage, + test.file_path.replace('./', ''), + errorMessageLine - 1 + ) + } else { + context.failed( + testItem, + errorMessage, + test.file_path.replace('./', ''), + errorMessageLine - 1 + ) + } } else if (test.status === "failed" && test.pending_message !== null) { // Handle pending test cases. - context.errored( - testItem, - test.pending_message, - test.file_path.replace('./', ''), - test.line_number - ) + context.skipped(testItem) } }; } diff --git a/src/testFactory.ts b/src/testFactory.ts index ccf6961..1249f28 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -40,14 +40,14 @@ export class TestFactory implements vscode.Disposable { this.log, this.workspace, this.controller, - this.config, + this.config as RspecConfig, this.testSuite ) : new MinitestTestRunner( this.log, this.workspace, this.controller, - this.config, + this.config as MinitestConfig, this.testSuite ) this.disposables.push(this.runner); diff --git a/src/testLoader.ts b/src/testLoader.ts index 78a9812..1c8cc53 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -18,6 +18,10 @@ export type ParsedTest = { pending_message?: string | null, exception?: any, type?: any, // what is this? + full_path?: string, // Minitest + klass?: string, // Minitest + method?: string, // Minitest + runnable?: string, // Minitest } /** @@ -138,7 +142,13 @@ export class TestLoader implements vscode.Disposable { ) log.debug("Test output parsed. Adding tests to test suite", tests) - this.getTestSuiteForFile(tests, testItem); + // TODO: Add option to list only tests for single file to minitest and remove filter below + log.debug(`testItem fsPath: ${testItem.uri?.fsPath}`) + var filteredTests = tests.filter((test) => { + log.debug(`filter: test file path: ${test.file_path}`) + return testItem.uri?.fsPath.endsWith(test.file_path) + }) + this.getTestSuiteForFile(filteredTests, testItem); } catch (e: any) { log.error("Failed to load tests", e) return Promise.reject(e) diff --git a/test/fixtures/minitest/.vscode/settings.json b/test/fixtures/minitest/.vscode/settings.json index ca07cde..61a3799 100644 --- a/test/fixtures/minitest/.vscode/settings.json +++ b/test/fixtures/minitest/.vscode/settings.json @@ -1,5 +1,6 @@ { "rubyTestExplorer": { "testFramework": "minitest" - } + }, + "rubyTestExplorer.minitestDirectory": "test" } diff --git a/test/fixtures/minitest/test/square_test.rb b/test/fixtures/minitest/test/square/square_test.rb similarity index 85% rename from test/fixtures/minitest/test/square_test.rb rename to test/fixtures/minitest/test/square/square_test.rb index c41a458..62a712a 100644 --- a/test/fixtures/minitest/test/square_test.rb +++ b/test/fixtures/minitest/test/square/square_test.rb @@ -1,4 +1,4 @@ -require_relative "test_helper" +require "test_helper" class SquareTest < Minitest::Test def test_square_2 diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index 62dab44..ee1c214 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -46,4 +46,13 @@ export class StubTestItemCollection implements vscode.TestItemCollection { get(itemId: string): vscode.TestItem | undefined { return this.testIds[itemId] } -} \ No newline at end of file + + toString(): string { + var output = [] + output.push("[") + this.forEach((item, _) => { output.push(item.id, ", ") }) + if (this.size > 0) output = output.slice(0, -1) + output.push("]") + return output.join("") + } +} diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 683a4a6..93a51bc 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -108,12 +108,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) testUriMatches(testItem, expectation.file) if (expectation.children && expectation.children.length > 0) { - expect(testItem.children.size).to.eq(expectation.children.length, `wrong number of children (id: ${expectation.id})`) - let i = 0; - testItem.children.forEach((child) => { - testItemMatches(child, expectation.children![i]) - i++ - }) + testItemCollectionMatches(testItem.children, expectation.children, testItem) } expect(testItem.canResolveChildren).to.be.false expect(testItem.label).to.eq(expectation.label, `label mismatch (id: ${expectation.id})`) @@ -141,12 +136,19 @@ export function testItemArrayMatches(testItems: readonly vscode.TestItem[], expe } /** - * Loops through an array of {@link vscode.TestItem TestItem}s and asserts whether each in turn matches the expectation with the same index + * Loops through a {@link vscode.TestItemCollection TestItemCollection} and asserts whether each in turn matches the expectation with the same index * @param testItems TestItems to check * @param expectation Array of {@link TestItemExpectation}s to compare to */ - export function testItemCollectionMatches(testItems: vscode.TestItemCollection, expectation: TestItemExpectation[]) { - expect(testItems.size).to.eq(expectation.length) +export function testItemCollectionMatches( + testItems: vscode.TestItemCollection, + expectation: TestItemExpectation[], + parent?: vscode.TestItem +) { + expect(testItems.size).to.eq( + expectation.length, + parent ? `Wrong number of children in item ${parent.id}\n\t${testItems.toString()}` : `Wrong number of items in collection\n\t${testItems.toString()}` + ) let i = 0; testItems.forEach((testItem: vscode.TestItem) => { testItemMatches(testItem, expectation[i]) diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 3afc62a..ed35d07 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -1,206 +1,291 @@ -import * as assert from 'assert'; -//import * as path from 'path'; -import 'mocha' +import * as vscode from 'vscode'; +import * as path from 'path' +import { anything, instance, verify } from 'ts-mockito' +import { expect } from 'chai'; -//import * as vscode from 'vscode'; +import { TestLoader } from '../../../src/testLoader'; +import { TestSuite } from '../../../src/testSuite'; +import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; +import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -suite('Extension Test for Minitest', () => { - test('Load all tests', async () => { - assert.fail("Not yet fixed for new API") - // const dirPath = vscode.workspace.workspaceFolders![0].uri.path - - // await controller.load() - - // assert.deepStrictEqual( - // controller.suite, - // { - // type: 'suite', - // id: 'root', - // label: 'minitest Minitest', - // children: [ - // { - // file: path.resolve(dirPath, "test/abs_test.rb"), - // id: "./test/abs_test.rb", - // label: "abs_test.rb", - // type: "suite", - // children: [ - // { - // file: path.resolve(dirPath, "test/abs_test.rb"), - // id: "./test/abs_test.rb[4]", - // label: "abs positive", - // line: 3, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "test/abs_test.rb"), - // id: "./test/abs_test.rb[8]", - // label: "abs 0", - // line: 7, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "test/abs_test.rb"), - // id: "./test/abs_test.rb[12]", - // label: "abs negative", - // line: 11, - // type: "test" - // } - // ] - // }, - // { - // file: path.resolve(dirPath, "test/square_test.rb"), - // id: "./test/square_test.rb", - // label: "square_test.rb", - // type: "suite", - // children: [ - // { - // file: path.resolve(dirPath, "test/square_test.rb"), - // id: "./test/square_test.rb[4]", - // label: "square 2", - // line: 3, - // type: "test" - // }, - // { - // file: path.resolve(dirPath, "test/square_test.rb"), - // id: "./test/square_test.rb[8]", - // label: "square 3", - // line: 7, - // type: "test" - // } - // ] - // } - // ] - // } as TestSuiteInfo - // ) +import { stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { StubTestController } from '../../stubs/stubTestController'; + +suite('Extension Test for Minitest', function() { + let testController: vscode.TestController + let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] + let config: MinitestConfig + let testRunner: MinitestTestRunner; + let testLoader: TestLoader; + let testSuite: TestSuite; + + let expectedPath = (file: string): string => { + return path.resolve( + 'test', + 'fixtures', + 'minitest', + 'test', + file) + } + + let abs_positive_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[4]", + label: "abs positive", + line: 3, + } + let abs_zero_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[8]", + label: "abs 0", + line: 7, + } + let abs_negative_expectation = { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb[12]", + label: "abs negative", + line: 11, + } + let square_2_expectation = { + id: "square/square_test.rb[4]", + file: expectedPath("square/square_test.rb"), + label: "square 2", + line: 3 + } + let square_3_expectation = { + id: "square/square_test.rb[8]", + file: expectedPath("square/square_test.rb"), + label: "square 3", + line: 7 + } + + this.beforeAll(function () { + if (vscode.workspace.workspaceFolders) { + console.debug("Found workspace folders (test):") + for (const folder of vscode.workspace.workspaceFolders) { + console.debug(` - ${folder.uri.fsPath}`) + } + } else { + console.debug("No workspace folders open") + } }) - test('run test success', async () => { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./test/square_test.rb[4]') - - // assert.deepStrictEqual( - // controller.testEvents['./test/square_test.rb[4]'], - // [ - // { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - // { state: "running", test: "./test/square_test.rb[4]", type: "test" }, - // { state: "passed", test: "./test/square_test.rb[4]", type: "test" }, - // { state: "passed", test: "./test/square_test.rb[4]", type: "test" } - // ] - // ) + this.beforeEach(async function () { + vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') + testController = new StubTestController() + + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string, label?: string) => + testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + testController.items.add(createTest("abs_test.rb")) + let squareFolder = createTest("square") + testController.items.add(squareFolder) + squareFolder.children.add(createTest("square/square_test.rb", "square_test.rb")) + + console.debug(`Workspace folder used in test: ${workspaceFolder.uri.fsPath}`) + config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) + console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) + console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) + + testSuite = new TestSuite(stdout_logger(), testController, config) + testRunner = new MinitestTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + }) + + test('Load tests on file resolve request', async function () { + // No tests in suite initially, only test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + children: [] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + children: [] + }, + ] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + children: [] + }, + ], + }, + ] + ) + }) + + test('Load all tests', async () => { + // TODO: Load all files without resolving them all manually here + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) + + const testSuite = testController.items + + testItemCollectionMatches(testSuite, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + children: [ + square_2_expectation, + square_3_expectation + ] + }, + ], + }, + ] + ) }) - test('run test failure', async () => { - assert.fail("Not yet fixed for new API") - // await controller.load() - // await controller.runTest('./test/square_test.rb[8]') - - // assert.deepStrictEqual( - // controller.testEvents['./test/square_test.rb[8]'][0], - // { state: "running", test: "./test/square_test.rb[8]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./test/square_test.rb[8]'][1], - // { state: "running", test: "./test/square_test.rb[8]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./test/square_test.rb[8]'][2], - // { state: "failed", test: "./test/square_test.rb[8]", type: "test" } - // ) - - // const lastEvent = controller.testEvents['./test/square_test.rb[8]'][3] - // assert.strictEqual(lastEvent.state, "failed") - // assert.strictEqual(lastEvent.line, undefined) - // assert.strictEqual(lastEvent.tooltip, undefined) - // assert.strictEqual(lastEvent.description, undefined) - // assert.ok(lastEvent.message?.startsWith("Expected: 9\n Actual: 6\n")) - - // assert.strictEqual(lastEvent.decorations!.length, 1) - // const decoration = lastEvent.decorations![0] - // assert.strictEqual(decoration.line, 8) - // assert.strictEqual(decoration.file, undefined) - // assert.strictEqual(decoration.hover, undefined) - // assert.strictEqual(decoration.message, "Expected: 9\n Actual: 6") + test('run test success', async function() { + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) + + let mockRequest = setupMockRequest(testSuite, "square/square_test.rb") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + + // Passed called twice per test in file during dry run + testItemMatches(args.passedArg(0)["testItem"], square_2_expectation) + testItemMatches(args.passedArg(1)["testItem"], square_2_expectation) + testItemMatches(args.passedArg(2)["testItem"], square_3_expectation) + testItemMatches(args.passedArg(3)["testItem"], square_3_expectation) + + // Passed called again for passing test but not for failing test + testItemMatches(args.passedArg(4)["testItem"], square_2_expectation) + verify(mockTestRun.passed(anything(), undefined)).times(5) }) - test('run test error', async () => { - assert.fail("Not yet fixed for new API") - // const controller = new DummyController() - - // const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - // const testHub = testExplorerExtension.exports; - - // testHub.registerTestController(controller); - - // await controller.load() - // await controller.runTest('./test/abs_test.rb[8]') - - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[8]'][0], - // { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[8]'][1], - // { state: "running", test: "./test/abs_test.rb[8]", type: "test" } - // ) - - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[8]'][2], - // { state: "failed", test: "./test/abs_test.rb[8]", type: "test" } - // ) - - // const lastEvent = controller.testEvents['./test/abs_test.rb[8]'][3] - // assert.strictEqual(lastEvent.state, "failed") - // assert.strictEqual(lastEvent.line, undefined) - // assert.strictEqual(lastEvent.tooltip, undefined) - // assert.strictEqual(lastEvent.description, undefined) - // assert.ok(lastEvent.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) - - // assert.strictEqual(lastEvent.decorations!.length, 1) - // const decoration = lastEvent.decorations![0] - // assert.strictEqual(decoration.line, 8) - // assert.strictEqual(decoration.file, undefined) - // assert.strictEqual(decoration.hover, undefined) - // assert.ok(decoration.message?.startsWith("RuntimeError: Abs for zero is not supported\n")) + test('run test failure', async function() { + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) + + let mockRequest = setupMockRequest(testSuite, "square/square_test.rb") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun).failedArg(0) + + testItemMatches(args.testItem, square_3_expectation) + + expect(args.message.message).to.contain("Expected: 9\n Actual: 6\n") + expect(args.message.actualOutput).to.be.undefined + expect(args.message.expectedOutput).to.be.undefined + expect(args.message.location?.range.start.line).to.eq(8) + expect(args.message.location?.uri.fsPath).to.eq(square_3_expectation.file) + expect(args.message.location?.uri.fsPath).to.eq(expectedPath("square/square_test.rb")) + + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) }) - test('run test skip', async () => { - assert.fail("Not yet fixed for new API") - // const controller = new DummyController() + test('run test error', async function() { + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + + let mockRequest = setupMockRequest(testSuite, "abs_test.rb") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) - // const testExplorerExtension = vscode.extensions.getExtension(testExplorerExtensionId)!; - // const testHub = testExplorerExtension.exports; + let mockTestRun = (testController as StubTestController).getMockTestRun() - // testHub.registerTestController(controller); + let args = testStateCaptors(mockTestRun).erroredArg(0) - // await controller.load() - // await controller.runTest('./test/abs_test.rb[12]') + testItemMatches(args.testItem, abs_zero_expectation) - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[12]'][0], - // { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - // ) + expect(args.message.message).to.match(/RuntimeError: Abs for zero is not supported/) + expect(args.message.actualOutput).to.be.undefined + expect(args.message.expectedOutput).to.be.undefined + expect(args.message.location?.range.start.line).to.eq(8) + expect(args.message.location?.uri.fsPath).to.eq(abs_zero_expectation.file) + expect(args.message.location?.uri.fsPath).to.eq(expectedPath("abs_test.rb")) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(0) + verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) + }) - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[12]'][1], - // { state: "running", test: "./test/abs_test.rb[12]", type: "test" } - // ) + test('run test skip', async function() { + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) - // assert.deepStrictEqual( - // controller.testEvents['./test/abs_test.rb[12]'][2], - // { state: "skipped", test: "./test/abs_test.rb[12]", type: "test" } - // ) + let mockRequest = setupMockRequest(testSuite, "abs_test.rb") + let request = instance(mockRequest) + let cancellationTokenSource = new vscode.CancellationTokenSource() + await testRunner.runHandler(request, cancellationTokenSource.token) - // const lastEvent = controller.testEvents['./test/abs_test.rb[12]'][3] - // assert.strictEqual(lastEvent.state, "skipped") - // assert.strictEqual(lastEvent.line, undefined) - // assert.strictEqual(lastEvent.tooltip, undefined) - // assert.strictEqual(lastEvent.description, undefined) - // assert.strictEqual(lastEvent.message, "Not implemented yet") + let mockTestRun = (testController as StubTestController).getMockTestRun() - // assert.strictEqual(lastEvent.decorations, undefined) + let args = testStateCaptors(mockTestRun) + testItemMatches(args.startedArg(0), { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }) + testItemMatches(args.skippedArg(0), abs_negative_expectation) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.skipped(anything())).times(1) }) }); diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 91d4890..cef0f4c 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -148,7 +148,8 @@ suite('Extension Test for RSpec', function() { ) }) - test('Load all tests', async function() { + test('Load all tests', async function () { + // TODO: Load all files without resolving them all manually here await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("subfolder/foo_spec.rb"))) diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 4a684fd..c700061 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -15,77 +15,179 @@ suite('TestLoader', function () { const config = mock() - before(function () { - console.log('before') - when(config.getRelativeTestDirectory()).thenReturn('spec') - }) + suite('#parseDryRunOutput()', function () { + suite('RSpec output', function () { + before(function () { + console.log('before') + when(config.getRelativeTestDirectory()).thenReturn('spec') + }) - beforeEach(function () { - testController = new StubTestController() - testSuite = new TestSuite(stdout_logger(), testController, instance(config)) - }) + beforeEach(function () { + testController = new StubTestController() + testSuite = new TestSuite(stdout_logger(), testController, instance(config)) + }) - suite('#parseDryRunOutput()', function () { - const examples: ParsedTest[] = [ - { - "id": "./spec/abs_spec.rb[1:1]", - "description": "finds the absolute value of 1", - "full_description": "Abs finds the absolute value of 1", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 4, - "type": null, - "pending_message": null - }, - { - "id": "./spec/abs_spec.rb[1:2]", - "description": "finds the absolute value of 0", - "full_description": "Abs finds the absolute value of 0", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 8, - "type": null, - "pending_message": null - }, - { - "id": "./spec/abs_spec.rb[1:3]", - "description": "finds the absolute value of -1", - "full_description": "Abs finds the absolute value of -1", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 12, - "type": null, - "pending_message": null - } - ] - const parameters = examples.map( - (spec: ParsedTest, i: number, examples: ParsedTest[]) => { - return { - index: i, - spec: spec, - expected_location: i + 11, - expected_id: `abs_spec.rb[1:${i + 1}]`, - expected_file_path: 'abs_spec.rb' + const examples: ParsedTest[] = [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": null + } + ] + const parameters = examples.map( + (spec: ParsedTest, i: number, examples: ParsedTest[]) => { + return { + index: i, + spec: spec, + expected_location: i + 11, + expected_id: `abs_spec.rb[1:${i + 1}]`, + expected_file_path: 'abs_spec.rb' + } + } + ) + + parameters.forEach(({ + index, + spec, + expected_location, + expected_id, + expected_file_path + }) => { + test(`parses specs correctly - ${spec["id"]}`, function () { + let parsedSpec = TestLoader.parseDryRunOutput( + noop_logger(), + testSuite, + [spec] + )[0] + expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') + expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') + expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') + }) + }) + }) + + suite('Minitest output', function () { + before(function () { + console.log('before') + when(config.getRelativeTestDirectory()).thenReturn('test') + }) + + beforeEach(function () { + testController = new StubTestController() + testSuite = new TestSuite(stdout_logger(), testController, instance(config)) + }) + + const examples: ParsedTest[] = [ + { + "description": "abs positive", + "full_description": "abs positive", + "file_path": "./test/abs_test.rb", + "full_path": "home/foo/test/fixtures/minitest/test/abs_test.rb", + "line_number": 4, + "klass": "AbsTest", + "method": "test_abs_positive", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[4]" + }, + { + "description": "abs 0", + "full_description": "abs 0", + "file_path": "./test/abs_test.rb", + "full_path": "/home/foo/test/fixtures/minitest/test/abs_test.rb", + "line_number": 8, + "klass": "AbsTest", + "method": "test_abs_0", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[8]" + }, + { + "description": "abs negative", + "full_description": "abs negative", + "file_path": "./test/abs_test.rb", + "full_path": "/home/foo/test/fixtures/minitest/test/abs_test.rb", + "line_number": 12, + "klass": "AbsTest", + "method": "test_abs_negative", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[12]" + }, + { + "description": "square 2", + "full_description": "square 2", + "file_path": "./test/square_test.rb", + "full_path": "/home/foo/test/fixtures/minitest/test/square_test.rb", + "line_number": 4, + "klass": "SquareTest", + "method": "test_square_2", + "runnable": "SquareTest", + "id": "./test/square_test.rb[4]" + }, + { + "description": "square 3", + "full_description": "square 3", + "file_path": "./test/square_test.rb", + "full_path": "/home/foo/test/fixtures/minitest/test/square_test.rb", + "line_number": 8, + "klass": "SquareTest", + "method": "test_square_3", + "runnable": "SquareTest", + "id": "./test/square_test.rb[8]" + } + ] + const parameters = examples.map( + (spec: ParsedTest, i: number, examples: ParsedTest[]) => { + return { + index: i, + spec: spec, + expected_location: examples[i]["line_number"], + expected_id: `${examples[i]["file_path"].replace('./test/', '')}[${examples[i]["line_number"]}]`, + expected_file_path: `${examples[i]["file_path"].replace('./test/', '')}` + } } - } - ) + ) - parameters.forEach(({ - index, - spec, - expected_location, - expected_id, - expected_file_path - }) => { - test(`parses specs correctly - ${spec["id"]}`, function () { - let parsedSpec = TestLoader.parseDryRunOutput( - noop_logger(), - testSuite, - [spec] - )[0] - expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') - expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') - expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') + parameters.forEach(({ + index, + spec, + expected_location, + expected_id, + expected_file_path + }) => { + test(`parses specs correctly - ${spec["id"]}`, function () { + let parsedSpec = TestLoader.parseDryRunOutput( + stdout_logger(), + testSuite, + [spec] + )[0] + expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') + expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') + expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') + }) }) }) }) diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index e47e164..58bc3f7 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -38,6 +38,7 @@ suite('TestSuite', function () { { arg: 'path/to/spec/folder/test-id', expected: 'folder/test-id' }, { arg: './path/to/spec/folder/test-id', expected: 'folder/test-id' }, { arg: './path/to/spec/abs_spec.rb[1:1]', expected: 'abs_spec.rb[1:1]' }, + { arg: './path/to/spec/abs_spec.rb[1]', expected: 'abs_spec.rb[1]' }, ]; parameters.forEach(({ arg, expected }) => { From bfeffb5ae9d4dfe14bc58f6977018c399a37b3b2 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 2 Dec 2022 13:05:09 +0000 Subject: [PATCH 040/108] Fix RSpec status parsing for errored tests --- src/rspec/rspecTestRunner.ts | 21 +++++++++++++++------ test/suite/rspec/rspec.test.ts | 5 +++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 41fe560..c300c26 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -108,12 +108,21 @@ export class RspecTestRunner extends TestRunner { }); } - context.failed( - testItem, - errorMessage, - filePath, - (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, - ) + if (test.exception.class.startsWith("RSpec")) { + context.failed( + testItem, + errorMessage, + filePath, + (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, + ) + } else { + context.errored( + testItem, + errorMessage, + filePath, + (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, + ) + } } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { // Handle pending test cases. context.skipped(testItem) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index cef0f4c..33547e6 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -305,7 +305,7 @@ suite('Extension Test for RSpec', function() { let mockTestRun = (testController as StubTestController).getMockTestRun() - let args = testStateCaptors(mockTestRun).failedArg(0) + let args = testStateCaptors(mockTestRun).erroredArg(0) // Actual failure report let expectation = { @@ -323,7 +323,8 @@ suite('Extension Test for RSpec', function() { expect(args.message.location?.uri.fsPath).to.eq(expectation.file) expect(args.message.location?.uri.fsPath).to.eq(expectedPath("abs_spec.rb")) verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) + verify(mockTestRun.failed(anything(), anything(), undefined)).times(0) + verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) }) test('run test skip', async function() { From 679aa2912db86aad3db5b3501e654bdc79051aaa Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 2 Dec 2022 13:45:19 +0000 Subject: [PATCH 041/108] Fix discoverAllFilesInWorkspace and use it in loadAllTests tests --- src/testLoader.ts | 30 +++++++++++--------- test/fixtures/minitest/.vscode/settings.json | 5 +++- test/fixtures/rspec/.vscode/settings.json | 5 +++- test/suite/minitest/minitest.test.ts | 13 ++++----- test/suite/rspec/rspec.test.ts | 6 ++-- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index 1c8cc53..8594094 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -80,9 +80,11 @@ export class TestLoader implements vscode.Disposable { this.testSuite.deleteTestItem(uri) }); + let testFiles = [] for (const file of await vscode.workspace.findFiles(pattern)) { - this.testSuite.getOrCreateTestItem(file); + testFiles.push(this.testSuite.getOrCreateTestItem(file)) } + await this.loadTests(testFiles) return watcher; } @@ -91,7 +93,7 @@ export class TestLoader implements vscode.Disposable { * Searches the configured test directory for test files, and calls createWatcher for * each one found. */ - discoverAllFilesInWorkspace() { + public async discoverAllFilesInWorkspace(): Promise { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) let testDir = this.config.getAbsoluteTestDirectory() log.debug(`testDir: ${testDir}`) @@ -109,8 +111,6 @@ export class TestLoader implements vscode.Disposable { log.debug("Setting up watchers with the following test patterns", patterns) return Promise.all(patterns.map(async (pattern) => await this.createWatcher(pattern))) - .then() - .catch(err => { log.error(err) }) } /** @@ -119,11 +119,11 @@ export class TestLoader implements vscode.Disposable { * * @return The full test suite. */ - public async loadTests(testItem: vscode.TestItem): Promise { + public async loadTests(testItems: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItem.id} (${this.config.frameworkName()})...`); + log.info(`Loading tests for ${testItems.length} items (${this.config.frameworkName()})...`); try { - let output = await this.testRunner.initTests([testItem]); + let output = await this.testRunner.initTests(testItems); log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); output = TestRunner.getJsonFromOutput(output); @@ -142,13 +142,15 @@ export class TestLoader implements vscode.Disposable { ) log.debug("Test output parsed. Adding tests to test suite", tests) - // TODO: Add option to list only tests for single file to minitest and remove filter below - log.debug(`testItem fsPath: ${testItem.uri?.fsPath}`) - var filteredTests = tests.filter((test) => { - log.debug(`filter: test file path: ${test.file_path}`) - return testItem.uri?.fsPath.endsWith(test.file_path) + testItems.forEach((testItem) => { + // TODO: Add option to list only tests for single file to minitest and remove filter below + log.debug(`testItem fsPath: ${testItem.uri?.fsPath}`) + var filteredTests = tests.filter((test) => { + log.debug(`filter: test file path: ${test.file_path}`) + return testItem.uri?.fsPath.endsWith(test.file_path) + }) + this.getTestSuiteForFile(filteredTests, testItem); }) - this.getTestSuiteForFile(filteredTests, testItem); } catch (e: any) { log.error("Failed to load tests", e) return Promise.reject(e) @@ -272,7 +274,7 @@ export class TestLoader implements vscode.Disposable { } log.info(`${testItem.id} has been edited, reloading tests.`); - await this.loadTests(testItem) + await this.loadTests([testItem]) } private configWatcher(): vscode.Disposable { diff --git a/test/fixtures/minitest/.vscode/settings.json b/test/fixtures/minitest/.vscode/settings.json index 61a3799..0fcb80c 100644 --- a/test/fixtures/minitest/.vscode/settings.json +++ b/test/fixtures/minitest/.vscode/settings.json @@ -2,5 +2,8 @@ "rubyTestExplorer": { "testFramework": "minitest" }, - "rubyTestExplorer.minitestDirectory": "test" + "rubyTestExplorer.minitestDirectory": "test", + "rubyTestExplorer.filePattern": [ + "*_test.rb" + ] } diff --git a/test/fixtures/rspec/.vscode/settings.json b/test/fixtures/rspec/.vscode/settings.json index 277195e..b79f474 100644 --- a/test/fixtures/rspec/.vscode/settings.json +++ b/test/fixtures/rspec/.vscode/settings.json @@ -2,5 +2,8 @@ "rubyTestExplorer": { "testFramework": "rspec" }, - "rubyTestExplorer.rspecDirectory": "spec" + "rubyTestExplorer.rspecDirectory": "spec", + "rubyTestExplorer.filePattern": [ + "*_spec.rb" + ] } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index ed35d07..70483f6 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -8,7 +8,7 @@ import { TestSuite } from '../../../src/testSuite'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { noop_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { @@ -72,6 +72,7 @@ suite('Extension Test for Minitest', function() { this.beforeEach(async function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') + vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) testController = new StubTestController() // Populate controller with test files. This would be done by the filesystem globs in the watchers @@ -87,9 +88,9 @@ suite('Extension Test for Minitest', function() { console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) - testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new MinitestTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new MinitestTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -152,9 +153,7 @@ suite('Extension Test for Minitest', function() { }) test('Load all tests', async () => { - // TODO: Load all files without resolving them all manually here - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) + await testLoader.discoverAllFilesInWorkspace() const testSuite = testController.items diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 33547e6..74a6831 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -41,6 +41,7 @@ suite('Extension Test for RSpec', function() { this.beforeEach(async function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') + vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) testController = new StubTestController() // Populate controller with test files. This would be done by the filesystem globs in the watchers @@ -149,10 +150,7 @@ suite('Extension Test for RSpec', function() { }) test('Load all tests', async function () { - // TODO: Load all files without resolving them all manually here - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("subfolder/foo_spec.rb"))) + await testLoader.discoverAllFilesInWorkspace() const testSuite = testController.items From a4730fca313dfb09809260c1886b4d4d1c23591d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 2 Dec 2022 13:52:27 +0000 Subject: [PATCH 042/108] Remove a lot of debug output --- test/suite/minitest/minitest.test.ts | 14 -------------- test/suite/rspec/rspec.test.ts | 16 ---------------- test/suite/unitTests/testLoader.test.ts | 10 ++++------ 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 70483f6..eaa70d2 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -59,17 +59,6 @@ suite('Extension Test for Minitest', function() { line: 7 } - this.beforeAll(function () { - if (vscode.workspace.workspaceFolders) { - console.debug("Found workspace folders (test):") - for (const folder of vscode.workspace.workspaceFolders) { - console.debug(` - ${folder.uri.fsPath}`) - } - } else { - console.debug("No workspace folders open") - } - }) - this.beforeEach(async function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) @@ -83,10 +72,7 @@ suite('Extension Test for Minitest', function() { testController.items.add(squareFolder) squareFolder.children.add(createTest("square/square_test.rb", "square_test.rb")) - console.debug(`Workspace folder used in test: ${workspaceFolder.uri.fsPath}`) config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) - console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) - console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new MinitestTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 74a6831..967a1e1 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -28,17 +28,6 @@ suite('Extension Test for RSpec', function() { file) } - this.beforeAll(function () { - if (vscode.workspace.workspaceFolders) { - console.debug("Found workspace folders (test):") - for (const folder of vscode.workspace.workspaceFolders) { - console.debug(` - ${folder.uri.fsPath}`) - } - } else { - console.debug("No workspace folders open") - } - }) - this.beforeEach(async function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) @@ -53,10 +42,7 @@ suite('Extension Test for RSpec', function() { testController.items.add(subfolderItem) subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) - console.debug(`Workspace folder used in test: ${workspaceFolder.uri.fsPath}`) config = new RspecConfig(path.resolve("ruby"), workspaceFolder) - console.debug(`relative test dir: ${config.getRelativeTestDirectory()}`) - console.debug(`absolute test dir: ${config.getAbsoluteTestDirectory()}`) testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) @@ -154,8 +140,6 @@ suite('Extension Test for RSpec', function() { const testSuite = testController.items - console.log(`testSuite: ${JSON.stringify(testSuite)}`) - testItemCollectionMatches(testSuite, [ { diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index c700061..84c6ebf 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import { Config } from "../../../src/config"; import { ParsedTest, TestLoader } from "../../../src/testLoader"; import { TestSuite } from "../../../src/testSuite"; -import { noop_logger, stdout_logger } from "../helpers"; +import { noop_logger } from "../helpers"; import { StubTestController } from '../../stubs/stubTestController'; suite('TestLoader', function () { @@ -18,13 +18,12 @@ suite('TestLoader', function () { suite('#parseDryRunOutput()', function () { suite('RSpec output', function () { before(function () { - console.log('before') when(config.getRelativeTestDirectory()).thenReturn('spec') }) beforeEach(function () { testController = new StubTestController() - testSuite = new TestSuite(stdout_logger(), testController, instance(config)) + testSuite = new TestSuite(noop_logger(), testController, instance(config)) }) const examples: ParsedTest[] = [ @@ -93,13 +92,12 @@ suite('TestLoader', function () { suite('Minitest output', function () { before(function () { - console.log('before') when(config.getRelativeTestDirectory()).thenReturn('test') }) beforeEach(function () { testController = new StubTestController() - testSuite = new TestSuite(stdout_logger(), testController, instance(config)) + testSuite = new TestSuite(noop_logger(), testController, instance(config)) }) const examples: ParsedTest[] = [ @@ -180,7 +178,7 @@ suite('TestLoader', function () { }) => { test(`parses specs correctly - ${spec["id"]}`, function () { let parsedSpec = TestLoader.parseDryRunOutput( - stdout_logger(), + noop_logger(), testSuite, [spec] )[0] From b5b2c106f804e0ebf0e52d48d2082315872da74a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 2 Dec 2022 13:55:34 +0000 Subject: [PATCH 043/108] Update Github workflow and add unit tests to it --- .github/workflows/test.yml | 7 +++++-- test/runFrameworkTests.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f84761..bbac89f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,12 +29,15 @@ jobs: run: | npm run build npm run package + - name: Run unit tests + run: | + xvfb-run -a node ./out/test/runFrameworkTests.js unitTests - name: Run minitest tests run: | - xvfb-run -a node ./out/test/runMinitestTests.js + xvfb-run -a node ./out/test/runFrameworkTests.js minitest - name: Run rspec tests run: | - xvfb-run -a node ./out/test/runRspecTests.js + xvfb-run -a node ./out/test/runFrameworkTests.js rspec - name: Run Ruby test run: | cd ruby && bundle exec rake diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 684124f..5d036c3 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -59,7 +59,7 @@ function printHelpAndExit() { }); console.log("") console.log("Example:") - console.log("node ./out/test/runTestSuites.js rspec") + console.log("node ./out/test/runFrameworkTests.js rspec") process.exit(1) } From 5b1dde4d5205c38be9406b553e6352e0eb2f687f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 2 Dec 2022 14:11:35 +0000 Subject: [PATCH 044/108] Clean up some comments/TODOs --- src/main.ts | 4 ---- src/rspec/rspecTestRunner.ts | 1 - src/testLoader.ts | 10 ++-------- src/testRunner.ts | 1 - 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5ceee3f..c2f75d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -77,10 +77,6 @@ export async function activate(context: vscode.ExtensionContext) { testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); - // TODO: Allow lazy-loading of child tests - below is taken from example in docs - // Custom handler for loading tests. The "test" argument here is undefined, - // but if we supported lazy-loading child test then this could be called with - // the test whose children VS Code wanted to load. controller.resolveHandler = async test => { log.debug('resolveHandler called', test) if (!test) { diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index c300c26..20a8e92 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -76,7 +76,6 @@ export class RspecTestRunner extends TestRunner { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { - // TODO: Parse additional test data context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { // Remove linebreaks from error message. diff --git a/src/testLoader.ts b/src/testLoader.ts index 8594094..08ff4f6 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -17,7 +17,7 @@ export type ParsedTest = { status?: string, pending_message?: string | null, exception?: any, - type?: any, // what is this? + type?: any, full_path?: string, // Minitest klass?: string, // Minitest method?: string, // Minitest @@ -100,12 +100,6 @@ export class TestLoader implements vscode.Disposable { let patterns: Array = [] this.config.getFilePattern().forEach(pattern => { - // TODO: Search all workspace folders (needs ability to exclude folders) - // if (vscode.workspace.workspaceFolders) { - // vscode.workspace.workspaceFolders!.forEach(async (workspaceFolder) => { - // patterns.push(new vscode.RelativePattern(workspaceFolder, '**/' + pattern)) - // }) - // } patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) }) @@ -171,7 +165,7 @@ export class TestLoader implements vscode.Disposable { log.debug("Parsing test", test) let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let test_location_string: string = test_location_array.join(''); - let location = parseInt(test_location_string); // TODO: check this isn't RSpec specific + let location = parseInt(test_location_string); let id = testSuite.normaliseTestId(test.id) let file_path = TestLoader.normaliseFilePath(testSuite, test.file_path) let parsedTest = { diff --git a/src/testRunner.ts b/src/testRunner.ts index 46f4cfd..5564519 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -132,7 +132,6 @@ export abstract class TestRunner implements vscode.Disposable { } }); - // TODO: Parse test IDs, durations, and failure message(s) from data this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); childProcessLogger.debug(data); From ca1ab9c3c7b48443f575ac23713676961c4ce79f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 27 Dec 2022 16:11:55 +0000 Subject: [PATCH 045/108] Change TestRunner.initTests to a method rather than a property --- src/minitest/minitestTestRunner.ts | 12 +++++++++--- src/rspec/rspecTestRunner.ts | 11 ++++++++--- src/testRunner.ts | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 35b540f..14374c3 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -13,7 +13,7 @@ export class MinitestTestRunner extends TestRunner { * * @return The raw output from the Minitest JSON formatter. */ - initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { + async initTests(testItems: vscode.TestItem[]): Promise { let cmd = `${(this.config as MinitestConfig).getTestCommand()} vscode:minitest:list`; // Allow a buffer of 64MB. @@ -25,6 +25,7 @@ export class MinitestTestRunner extends TestRunner { this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); + let output: string | undefined childProcess.exec(cmd, execArgs, (err, stdout) => { if (err) { this.log.error(`Error while finding Minitest test suite: ${err.message}`); @@ -34,9 +35,14 @@ export class MinitestTestRunner extends TestRunner { vscode.window.showErrorMessage(err.message); throw err; } - resolve(stdout); + output = stdout; }); - }); + + if (!output) { + "No output returned from child process" + } + return output as string + }; protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { let line = testItem.id.split(':').pop(); diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 20a8e92..872f9d9 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -12,7 +12,7 @@ export class RspecTestRunner extends TestRunner { * * @return The raw output from the RSpec JSON formatter. */ - initTests = async (testItems: vscode.TestItem[]) => new Promise((resolve, reject) => { + async initTests(testItems: vscode.TestItem[]): Promise { let cfg = this.config as RspecConfig let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; @@ -31,6 +31,7 @@ export class RspecTestRunner extends TestRunner { maxBuffer: 8192 * 8192, }; + let output: string | undefined childProcess.exec(cmd, execArgs, (err, stdout) => { if (err) { if (err.message.includes('deprecated')) { @@ -62,9 +63,13 @@ export class RspecTestRunner extends TestRunner { throw err; } } - resolve(stdout); + output = stdout; }); - }); + if (!output) { + throw "No output returned from child process" + } + return output as string + }; /** * Handles test state based on the output returned by the custom RSpec formatter. diff --git a/src/testRunner.ts b/src/testRunner.ts index 5564519..038f8c3 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -36,7 +36,7 @@ export abstract class TestRunner implements vscode.Disposable { * Initialise the test framework, parse tests (without executing) and retrieve the output * @return Stdout outpu from framework initialisation */ - abstract initTests: (testItems: vscode.TestItem[]) => Promise; + abstract initTests(testItems: vscode.TestItem[]): Promise public dispose() { this.killChild(); From 754d8211d9dfb992e99ff698836f4343ffb95664 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 27 Dec 2022 16:12:34 +0000 Subject: [PATCH 046/108] Remove tests from collections if not in test runner output --- src/testLoader.ts | 4 +- src/testSuite.ts | 3 +- test/stubs/stubTestItemCollection.ts | 1 + test/suite/unitTests/testLoader.test.ts | 51 +++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index 08ff4f6..7b0cf77 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -201,6 +201,7 @@ export class TestLoader implements vscode.Disposable { let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); + let testItems: vscode.TestItem[] = [] tests.forEach((test) => { log.debug(`Building test: ${test.id}`) // RSpec provides test ids like "file_name.rb[1:2:3]". @@ -234,8 +235,9 @@ export class TestLoader implements vscode.Disposable { childTestItem.label = description childTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); - testItem.children.add(childTestItem); + testItems.push(childTestItem); }); + testItem.children.replace(testItems) } diff --git a/src/testSuite.ts b/src/testSuite.ts index 90be246..1e8a5d0 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -163,9 +163,9 @@ export class TestSuite { if (fileId.startsWith(path.sep)) { fileId = fileId.substring(1) } - log.debug(`Getting file collection ${fileId}`) let childCollection = collection.get(fileId)?.children if (!childCollection) { + log.debug(`TestItem for file ${fileId} not in parent collection`) if (!createIfMissing) return undefined let child = this.createTestItem( collection, @@ -174,6 +174,7 @@ export class TestSuite { ) childCollection = child.children } + log.debug(`Got TestItem for file ${fileId} from parent collection`) collection = childCollection } // else test item is the file so return the file's parent diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index ee1c214..8eda3eb 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -5,6 +5,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { get size(): number { return Object.keys(this.testIds).length; }; replace(items: readonly vscode.TestItem[]): void { + this.testIds = {} items.forEach(item => { this.testIds[item.id] = item }) diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts index 84c6ebf..aec8f02 100644 --- a/test/suite/unitTests/testLoader.test.ts +++ b/test/suite/unitTests/testLoader.test.ts @@ -2,10 +2,12 @@ import { expect } from "chai"; import { before, beforeEach } from 'mocha'; import { instance, mock, when } from 'ts-mockito' import * as vscode from 'vscode' +import * as path from 'path' import { Config } from "../../../src/config"; import { ParsedTest, TestLoader } from "../../../src/testLoader"; import { TestSuite } from "../../../src/testSuite"; +import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; import { noop_logger } from "../helpers"; import { StubTestController } from '../../stubs/stubTestController'; @@ -189,4 +191,53 @@ suite('TestLoader', function () { }) }) }) + + suite('getTestSuiteForFile', function() { + let mockTestRunner: RspecTestRunner + let testRunner: RspecTestRunner + let testLoader: TestLoader + let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] + let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') + let id = "abs_spec.rb" + let abs_spec_item: vscode.TestItem + let createTestItem = (id: string): vscode.TestItem => { + return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) + } + + this.beforeAll(function () { + when(config.getRelativeTestDirectory()).thenReturn('spec') + when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) + }) + + this.beforeEach(function () { + mockTestRunner = mock(RspecTestRunner) + testRunner = instance(mockTestRunner) + testController = new StubTestController() + testSuite = new TestSuite(noop_logger(), testController, instance(config)) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) + abs_spec_item = createTestItem(id) + testController.items.add(abs_spec_item) + }) + + test('creates test items from output', function () { + expect(abs_spec_item.children.size).to.eq(0) + + testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + expect(abs_spec_item.children.size).to.eq(1) + }) + + test('removes test items not in output', function () { + let missing_id = "abs_spec.rb[3:1]" + let missing_child_item = createTestItem(missing_id) + abs_spec_item.children.add(missing_child_item) + expect(abs_spec_item.children.size).to.eq(1) + + testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + expect(abs_spec_item.children.size).to.eq(1) + expect(abs_spec_item.children.get(missing_id)).to.be.undefined + expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined + }) + }) }) From ff426159a5c1ed38cddd10839caffa5fbd04ef35 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 30 Dec 2022 20:54:36 +0000 Subject: [PATCH 047/108] Tidy up handling of child processes and extract function to get command for listing test details --- src/minitest/minitestTestRunner.ts | 35 ++++---- src/rspec/rspecTestRunner.ts | 85 +++++++++--------- src/testRunner.ts | 138 +++++++++++++++++------------ test/suite/rspec/rspec.test.ts | 6 +- 4 files changed, 147 insertions(+), 117 deletions(-) diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 14374c3..627f014 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -14,7 +14,7 @@ export class MinitestTestRunner extends TestRunner { * @return The raw output from the Minitest JSON formatter. */ async initTests(testItems: vscode.TestItem[]): Promise { - let cmd = `${(this.config as MinitestConfig).getTestCommand()} vscode:minitest:list`; + let cmd = this.getListTestsCommand(testItems) // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { @@ -25,25 +25,26 @@ export class MinitestTestRunner extends TestRunner { this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); - let output: string | undefined - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding Minitest test suite: ${err.message}`); - this.log.error(`Output: ${stdout}`); - // Show an error message. - vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); - vscode.window.showErrorMessage(err.message); - throw err; - } - output = stdout; + let output: Promise = new Promise((resolve, reject) => { + childProcess.exec(cmd, execArgs, (err, stdout) => { + if (err) { + this.log.error(`Error while finding Minitest test suite: ${err.message}`); + this.log.error(`Output: ${stdout}`); + // Show an error message. + vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); + vscode.window.showErrorMessage(err.message); + reject(err); + } + resolve(stdout); + }); }); - - if (!output) { - "No output returned from child process" - } - return output as string + return await output }; + protected getListTestsCommand(testItems?: vscode.TestItem[]): string { + return `${(this.config as MinitestConfig).getTestCommand()} vscode:minitest:list` + } + protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { let line = testItem.id.split(':').pop(); let relativeLocation = `${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}` diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 872f9d9..7f6f23b 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -13,13 +13,7 @@ export class RspecTestRunner extends TestRunner { * @return The raw output from the RSpec JSON formatter. */ async initTests(testItems: vscode.TestItem[]): Promise { - let cfg = this.config as RspecConfig - let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; - - testItems.forEach((item) => { - let testPath = `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` - cmd = `${cmd} "${testPath}"` - }) + let cmd = this.getListTestsCommand(testItems) this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); this.log.debug(`cwd: ${__dirname}`) @@ -31,44 +25,42 @@ export class RspecTestRunner extends TestRunner { maxBuffer: 8192 * 8192, }; - let output: string | undefined - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - if (err.message.includes('deprecated')) { - this.log.warn(`Warning while finding RSpec test suite: ${err.message}`) - } else { - this.log.error(`Error while finding RSpec test suite: ${err.message}`); - // Show an error message. - vscode.window.showWarningMessage( - "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", - "View error message" - ).then(selection => { - if (selection === "View error message") { - let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); - let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); - - if (outputJson.messages.length > 0) { - let outputJsonString = outputJson.messages.join("\n\n"); - let outputJsonArray = outputJsonString.split("\n"); - outputJsonArray.forEach((line: string) => { - outputChannel.appendLine(line); - }) - } else { - outputChannel.append(err.message); + let output: Promise = new Promise((resolve, reject) => { + childProcess.exec(cmd, execArgs, (err, stdout) => { + if (err) { + if (err.message.includes('deprecated')) { + this.log.warn(`Warning while finding RSpec test suite: ${err.message}`) + } else { + this.log.error(`Error while finding RSpec test suite: ${err.message}`); + // Show an error message. + vscode.window.showWarningMessage( + "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", + "View error message" + ).then(selection => { + if (selection === "View error message") { + let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); + let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); + + if (outputJson.messages.length > 0) { + let outputJsonString = outputJson.messages.join("\n\n"); + let outputJsonArray = outputJsonString.split("\n"); + outputJsonArray.forEach((line: string) => { + outputChannel.appendLine(line); + }) + } else { + outputChannel.append(err.message); + } + outputChannel.show(false); } - outputChannel.show(false); - } - }); + }); - throw err; + reject(err); + } } - } - output = stdout; + resolve(stdout); + }); }); - if (!output) { - throw "No output returned from child process" - } - return output as string + return await output }; /** @@ -133,6 +125,17 @@ export class RspecTestRunner extends TestRunner { } }; + protected getListTestsCommand(testItems?: vscode.TestItem[]): string { + let cfg = this.config as RspecConfig + let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; + + testItems?.forEach((item) => { + let testPath = `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` + cmd = `${cmd} "${testPath}"` + }) + return cmd + } + protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` }; diff --git a/src/testRunner.ts b/src/testRunner.ts index 038f8c3..eddb340 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -114,50 +114,67 @@ export abstract class TestRunner implements vscode.Disposable { * @param process A process running the tests. * @return A promise that resolves when the test run completes. */ - handleChildProcess = async (process: childProcess.ChildProcess, context: TestRunContext) => new Promise((resolve, reject) => { + async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; - let childProcessLogger = this.log.getChildLogger({ label: `ChildProcess(${context.config.frameworkName()})` }) + let log = this.log.getChildLogger({ label: `ChildProcess(${context.config.frameworkName()})` }) + let output: Promise = new Promise((resolve, reject) => { + let buffer: string | undefined + + process.on('exit', () => { + log.info('Child process has exited. Sending test run finish event.'); + this.currentChildProcess = undefined; + if(buffer) { + resolve(buffer); + } else { + let error = new Error("No output returned from child process") + log.error(error.message) + reject(error) + } + }); - this.currentChildProcess.on('exit', () => { - childProcessLogger.info('Child process has exited. Sending test run finish event.'); - this.currentChildProcess = undefined; - resolve('{}'); - }); + process.stderr!.pipe(split2()).on('data', (data) => { + data = data.toString(); + log.debug(data); + if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { + this.debugCommandStartedResolver() + } + }); - this.currentChildProcess.stderr!.pipe(split2()).on('data', (data) => { - data = data.toString(); - childProcessLogger.debug(data); - if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { - this.debugCommandStartedResolver() - } - }); + process.stdout!.pipe(split2()).on('data', (data) => { + data = data.toString(); + log.debug(data); + let markTestStatus = (fn: (test: vscode.TestItem) => void, testId: string) => { + testId = this.testSuite.normaliseTestId(testId) + let test = this.testSuite.getOrCreateTestItem(testId) + //context.passed(test) + // why does this not work? + //fn(test) + } + if (data.startsWith('PASSED:')) { + log.debug(`Received test status - PASSED`, data) + data = data.replace('PASSED: ', ''); + markTestStatus(test => context.passed(test), data) + } else if (data.startsWith('FAILED:')) { + log.debug(`Received test status - FAILED`, data) + data = data.replace('FAILED: ', ''); + markTestStatus(test => context.failed(test, "", "", 0), data) + } else if (data.startsWith('RUNNING:')) { + log.debug(`Received test status - RUNNING`, data) + data = data.replace('RUNNING: ', ''); + markTestStatus(test => context.started(test), data) + } else if (data.startsWith('PENDING:')) { + log.debug(`Received test status - PENDING`, data) + data = data.replace('PENDING: ', ''); + markTestStatus(test => context.enqueued(test), data) + } + if (data.includes('START_OF_TEST_JSON')) { + buffer = data; + } + }); + }) - this.currentChildProcess.stdout!.pipe(split2()).on('data', (data) => { - data = data.toString(); - childProcessLogger.debug(data); - let markTestStatus = (fn: (test: vscode.TestItem) => void, testId: string) => { - testId = this.testSuite.normaliseTestId(testId) - let test = this.testSuite.getOrCreateTestItem(testId) - context.passed(test) - } - if (data.startsWith('PASSED:')) { - data = data.replace('PASSED: ', ''); - markTestStatus(test => context.passed(test), data) - } else if (data.startsWith('FAILED:')) { - data = data.replace('FAILED: ', ''); - markTestStatus(test => context.failed(test, "", "", 0), data) - } else if (data.startsWith('RUNNING:')) { - data = data.replace('RUNNING: ', ''); - markTestStatus(test => context.started(test), data) - } else if (data.startsWith('PENDING:')) { - data = data.replace('PENDING: ', ''); - markTestStatus(test => context.enqueued(test), data) - } - if (data.includes('START_OF_TEST_JSON')) { - resolve(data); - } - }); - }); + return await output + }; /** * Test run handler @@ -180,14 +197,15 @@ export abstract class TestRunner implements vscode.Disposable { this.config, debuggerConfig ) + let log = this.log.getChildLogger({ label: `runHandler` }) try { const queue: vscode.TestItem[] = []; if (debuggerConfig) { - this.log.info(`Debugging test(s) ${JSON.stringify(request.include)}`); + log.info(`Debugging test(s) ${JSON.stringify(request.include)}`); if (!this.workspace) { - this.log.error("Cannot debug without a folder opened") + log.error("Cannot debug without a folder opened") context.testRun.end() return } @@ -198,20 +216,20 @@ export abstract class TestRunner implements vscode.Disposable { await this.debugCommandStarted() debugSession = await this.startDebugging(debuggerConfig); } catch (err) { - this.log.error('Failed starting the debug session - aborting', err); + log.error('Failed starting the debug session - aborting', err); this.killChild(); return; } const subscription = this.onDidTerminateDebugSession((session) => { if (debugSession != session) return; - this.log.info('Debug session ended'); + log.info('Debug session ended'); this.killChild(); // terminate the test run subscription.dispose(); }); } else { - this.log.info(`Running test(s) ${JSON.stringify(request.include)}`); + log.info(`Running test(s) ${JSON.stringify(request.include)}`); } // Loop through all included tests, or all known tests, and add them to our queue @@ -237,7 +255,7 @@ export abstract class TestRunner implements vscode.Disposable { }); } if (token.isCancellationRequested) { - this.log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } else { await this.runNode(null, context); @@ -377,12 +395,11 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test suite. */ - runTestFramework = async (testCommand: string, type: string, context: TestRunContext) => - new Promise(async (resolve, reject) => { + async runTestFramework(testCommand: string, type: string, context: TestRunContext): Promise { this.log.info(`Running test suite: ${type}`); - resolve(await this.spawnCancellableChild(testCommand, context)) - }); + return await this.spawnCancellableChild(testCommand, context) + }; /** * Spawns a child process to run a command, that will be killed @@ -458,21 +475,30 @@ export abstract class TestRunner implements vscode.Disposable { context) } + /** + * Gets the command to get information about tests. + * + * @param testItems Optional array of tests to list. If missing, all tests should be + * listed + * @return The command to run to get test details + */ + protected abstract getListTestsCommand(testItems?: vscode.TestItem[]): string; + /** * Gets the command to run a single test. * - * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. + * @param testItem A TestItem of a single test to be run * @param context Test run context - * @return The raw output from running the test. + * @return The command to run a single test. */ protected abstract getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string; /** * Gets the command to run tests in a given file. * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. + * @param testItem A TestItem of a file containing tests * @param context Test run context - * @return The raw output from running the tests. + * @return The command to run all tests in a given file. */ protected abstract getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string; @@ -480,14 +506,14 @@ export abstract class TestRunner implements vscode.Disposable { * Gets the command to run the full test suite for the current workspace. * * @param context Test run context - * @return The raw output from running the test suite. + * @return The command to run the full test suite for the current workspace. */ protected abstract getFullTestSuiteCommand(context: TestRunContext): string; /** * Handles test state based on the output returned by the test command. * - * @param test The test that we want to handle. + * @param test The parsed output from running the test * @param context Test run context */ protected abstract handleStatus(test: ParsedTest, context: TestRunContext): void; diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 967a1e1..5314490 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -8,7 +8,7 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { noop_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -44,8 +44,8 @@ suite('Extension Test for RSpec', function() { config = new RspecConfig(path.resolve("ruby"), workspaceFolder) - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testSuite = new TestSuite(stdout_logger(), testController, config) + testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) From 0e5b0fac700e2e9692f2dc31881c0d7315a66422 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 31 Dec 2022 00:10:22 +0000 Subject: [PATCH 048/108] WIP: Fix status handling and test information parsing to work correctly in all cases Dry run and actual run of tests were done differently and that all needs merging together --- TODO | 3 + src/rspec/rspecTestRunner.ts | 5 +- src/testRunContext.ts | 4 +- src/testRunner.ts | 59 ++++--- test/suite/helpers.ts | 36 ++++- test/suite/rspec/rspec.test.ts | 270 ++++++++++++++++++--------------- 6 files changed, 227 insertions(+), 150 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..c950c1e --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +* Move logic for setting test info from test runner output from TestLoader into TestRunner and use it before we call handleStatus on line 371 +* Fix up minitest specs like I did for the RSpec ones +* Get rid of initTests and do it using TestRunner.spawnCancellable child like the actual test runs do diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 7f6f23b..3c4ff95 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -70,9 +70,11 @@ export class RspecTestRunner extends TestRunner { * @param context Test run context */ handleStatus(test: ParsedTest, context: TestRunContext): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + let log = this.log.getChildLogger({ label: "handleStatus" }) + log.debug(`Handling status of test: ${JSON.stringify(test)}`); let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { + log.debug("Passed", testItem.id) context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { // Remove linebreaks from error message. @@ -121,6 +123,7 @@ export class RspecTestRunner extends TestRunner { } } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { // Handle pending test cases. + log.debug("Skipped", testItem.id) context.skipped(testItem) } }; diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 7628c8d..f5913ad 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -9,6 +9,7 @@ import { Config } from './config' */ export class TestRunContext { public readonly testRun: vscode.TestRun + public readonly log: IChildLogger /** * Create a new context @@ -20,13 +21,14 @@ export class TestRunContext { * @param debuggerConfig A VS Code debugger configuration. */ constructor( - public readonly log: IChildLogger, + readonly rootLog: IChildLogger, public readonly token: vscode.CancellationToken, readonly request: vscode.TestRunRequest, readonly controller: vscode.TestController, public readonly config: Config, public readonly debuggerConfig?: vscode.DebugConfiguration ) { + this.log = rootLog.getChildLogger({ label: "TestRunContext" }) this.testRun = controller.createTestRun(request) } diff --git a/src/testRunner.ts b/src/testRunner.ts index eddb340..c767ae6 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -143,29 +143,23 @@ export abstract class TestRunner implements vscode.Disposable { process.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); log.debug(data); - let markTestStatus = (fn: (test: vscode.TestItem) => void, testId: string) => { + let getTest = (testId: string): vscode.TestItem => { testId = this.testSuite.normaliseTestId(testId) - let test = this.testSuite.getOrCreateTestItem(testId) - //context.passed(test) - // why does this not work? - //fn(test) + return this.testSuite.getOrCreateTestItem(testId) } if (data.startsWith('PASSED:')) { log.debug(`Received test status - PASSED`, data) - data = data.replace('PASSED: ', ''); - markTestStatus(test => context.passed(test), data) + context.passed(getTest(data.replace('PASSED: ', ''))) } else if (data.startsWith('FAILED:')) { log.debug(`Received test status - FAILED`, data) - data = data.replace('FAILED: ', ''); - markTestStatus(test => context.failed(test, "", "", 0), data) + let testItem = getTest(data.replace('FAILED: ', '')) + context.failed(testItem, "", testItem.uri?.fsPath || "", testItem.range?.start.line || 0) } else if (data.startsWith('RUNNING:')) { log.debug(`Received test status - RUNNING`, data) - data = data.replace('RUNNING: ', ''); - markTestStatus(test => context.started(test), data) + context.started(getTest(data.replace('RUNNING: ', ''))) } else if (data.startsWith('PENDING:')) { log.debug(`Received test status - PENDING`, data) - data = data.replace('PENDING: ', ''); - markTestStatus(test => context.enqueued(test), data) + context.enqueued(getTest(data.replace('PENDING: ', ''))) } if (data.includes('START_OF_TEST_JSON')) { buffer = data; @@ -234,11 +228,13 @@ export abstract class TestRunner implements vscode.Disposable { // Loop through all included tests, or all known tests, and add them to our queue if (request.include) { + log.debug(`${request.include.length} tests in request`); request.include.forEach(test => queue.push(test)); - // For every test that was queued, try to run it. Call run.passed() or run.failed() + // For every test that was queued, try to run it while (queue.length > 0 && !token.isCancellationRequested) { const test = queue.pop()!; + log.debug(`Running test from queue ${test.id}`); // Skip tests the user asked to exclude if (request.exclude?.includes(test)) { @@ -258,6 +254,7 @@ export abstract class TestRunner implements vscode.Disposable { log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } else { + log.debug('Running all tests in suite'); await this.runNode(null, context); } } @@ -314,29 +311,35 @@ export abstract class TestRunner implements vscode.Disposable { node: vscode.TestItem | null, context: TestRunContext ): Promise { - let log = this.log.getChildLogger({label: "runNode"}) + let log = this.log.getChildLogger({label: this.runNode.name}) // Special case handling for the root suite, since it can be run // with runFullTestSuite() try { if (node == null) { log.debug("Running all tests") this.controller.items.forEach((testSuite) => { - this.enqueTestAndChildren(testSuite, context) + //this.enqueTestAndChildren(testSuite, context) + // Mark selected tests as started + this.markTestAndChildrenStarted(testSuite, context) }) let testOutput = await this.runFullTestSuite(context); this.parseAndHandleTestOutput(testOutput, context) // If the suite is a file, run the tests as a file rather than as separate tests. } else if (node.label.endsWith('.rb')) { log.debug(`Running test file: ${node.id}`) - // Mark selected tests as enqueued - this.enqueTestAndChildren(node, context) - context.started(node) + // Mark selected tests as enqueued - not really much point + //this.enqueTestAndChildren(node, context) + + // Mark selected tests as started + this.markTestAndChildrenStarted(node, context) + let testOutput = await this.runTestFile(node, context); this.parseAndHandleTestOutput(testOutput, context) } else { - if (node.uri !== undefined && node.range !== undefined) { + log.debug(`Running single test: ${node.id}`) + if (node.uri !== undefined) { log.debug(`Running single test: ${node.id}`) context.started(node) @@ -345,6 +348,8 @@ export abstract class TestRunner implements vscode.Disposable { let testOutput = await this.runSingleTest(node, context); this.parseAndHandleTestOutput(testOutput, context) + } else { + log.error("test missing file path") } } } finally { @@ -353,7 +358,7 @@ export abstract class TestRunner implements vscode.Disposable { } private parseAndHandleTestOutput(testOutput: string, context: TestRunContext) { - let log = this.log.getChildLogger({label: 'parseAndHandleTestOutput'}) + let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); log.debug('Parsing the below JSON:'); log.debug(`${testOutput}`); @@ -379,14 +384,22 @@ export abstract class TestRunner implements vscode.Disposable { * Mark a test node and all its children as being queued for execution */ private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { - let log = this.log.getChildLogger({label: "enqueTestAndChildren"}) - log.debug(`enqueing test ${test.id}`) context.enqueued(test); if (test.children && test.children.size > 0) { test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) } } + /** + * Mark a test node and all its children as being queued for execution + */ + private markTestAndChildrenStarted(test: vscode.TestItem, context: TestRunContext) { + context.started(test); + if (test.children && test.children.size > 0) { + test.children.forEach(child => { this.markTestAndChildrenStarted(child, context) }) + } + } + /** * Runs the test framework with the given command. * diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 93a51bc..1f6c012 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -88,6 +88,17 @@ export type TestItemExpectation = { children?: TestItemExpectation[] } +/** + * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values + */ +export type TestFailureExpectation = { + testItem: TestItemExpectation, + message?: string, + actualOutput?: string, + expectedOutput?: string, + line?: number, +} + export function testUriMatches(testItem: vscode.TestItem, path?: string) { if (path) { expect(testItem.uri).to.not.be.undefined @@ -156,6 +167,26 @@ export function testItemCollectionMatches( }) } +export function verifyFailure( + index: number, + captor: ArgCaptor3, + expectation: TestFailureExpectation): void +{ + let failure = captor.byCallIndex(index) + let testItem = failure[0] + let failureDetails = failure[1] + testItemMatches(testItem, expectation.testItem) + if (expectation.message) { + expect(failureDetails.message).to.contain(expectation.message) + } else { + expect(failureDetails.message).to.eq('') + } + expect(failureDetails.actualOutput).to.eq(expectation.actualOutput) + expect(failureDetails.expectedOutput).to.eq(expectation.expectedOutput) + expect(failureDetails.location?.range.start.line).to.eq(expectation.line || expectation.testItem.line || 0) + expect(failureDetails.location?.uri.fsPath).to.eq(expectation.testItem.file) +} + export function setupMockTestController(): vscode.TestController { let mockTestController = mock() let createTestItem = (id: string, label: string, uri?: vscode.Uri | undefined) => { @@ -182,10 +213,7 @@ export function setupMockTestController(): vscode.TestController { export function setupMockRequest(testSuite: TestSuite, testId?: string): vscode.TestRunRequest { let mockRequest = mock() if (testId) { - let testItem = testSuite.getTestItem(testId) - if (testItem === undefined) { - throw new Error("Couldn't find test") - } + let testItem = testSuite.getOrCreateTestItem(testId) when(mockRequest.include).thenReturn([testItem]) } else { when(mockRequest.include).thenReturn([]) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 5314490..69ca60c 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -8,7 +8,7 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -28,7 +28,7 @@ suite('Extension Test for RSpec', function() { file) } - this.beforeEach(async function () { + this.beforeEach(function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) testController = new StubTestController() @@ -44,7 +44,7 @@ suite('Extension Test for RSpec', function() { config = new RspecConfig(path.resolve("ruby"), workspaceFolder) - testSuite = new TestSuite(stdout_logger(), testController, config) + testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) @@ -210,125 +210,153 @@ suite('Extension Test for RSpec', function() { ) }) - test('run test success', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) - - let mockRequest = setupMockRequest(testSuite, "square_spec.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun) - let expectation = { - id: "square_spec.rb[1:1]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 2", - line: 3 - } - - // Passed called once per test in file during dry run - testItemMatches(args.passedArg(0)["testItem"], expectation) - testItemMatches( - args.passedArg(1)["testItem"], - { - id: "square_spec.rb[1:2]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 3", - line: 7 - } - ) - - // Passed called again for passing test but not for failing test - testItemMatches(args.passedArg(2)["testItem"], expectation) - verify(mockTestRun.passed(anything(), undefined)).times(3) - }) - - test('run test failure', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) - - let mockRequest = setupMockRequest(testSuite, "square_spec.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun).failedArg(0) - - // Actual failure report - let expectation = { - id: "square_spec.rb[1:2]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 3", - line: 7 - } - testItemMatches(args.testItem, expectation) - - expect(args.message.message).to.contain("RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n") - expect(args.message.actualOutput).to.be.undefined - expect(args.message.expectedOutput).to.be.undefined - expect(args.message.location?.range.start.line).to.eq(8) - expect(args.message.location?.uri.fsPath).to.eq(expectation.file) - expect(args.message.location?.uri.fsPath).to.eq(expectedPath("square_spec.rb")) - - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) - }) - - test('run test error', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - - let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:2]") - let request = instance(mockRequest) + suite('status events', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun).erroredArg(0) - - // Actual failure report - let expectation = { - id: "abs_spec.rb[1:2]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 0", - line: 7, - } - testItemMatches(args.testItem, expectation) - - expect(args.message.message).to.match(/RuntimeError:\nAbs for zero is not supported/) - expect(args.message.actualOutput).to.be.undefined - expect(args.message.expectedOutput).to.be.undefined - expect(args.message.location?.range.start.line).to.eq(8) - expect(args.message.location?.uri.fsPath).to.eq(expectation.file) - expect(args.message.location?.uri.fsPath).to.eq(expectedPath("abs_spec.rb")) - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(0) - verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) - }) - - test('run test skip', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:3]") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun) - let expectation = { - id: "abs_spec.rb[1:3]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of -1", - line: 11 - } - testItemMatches(args.startedArg(0), expectation) - testItemMatches(args.skippedArg(0), expectation) - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.skipped(anything())).times(1) + suite('file with passing and failing specs', function() { + let mockTestRun: vscode.TestRun + + this.beforeAll(async function() { + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) + + let mockRequest = setupMockRequest(testSuite, "square_spec.rb") + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() + }) + + test('enqueued status event', async function() { + // Not really a useful status event unless you can queue up tests and run only + // parts of the queue at a time - perhaps in the future + verify(mockTestRun.enqueued(anything())).times(0) + }) + + test('started status event', function() { + let args = testStateCaptors(mockTestRun) + + // Assert that all specs and the file are marked as started + expect(args.startedArg(0).id).to.eq("square_spec.rb") + expect(args.startedArg(1).id).to.eq("square_spec.rb[1:1]") + expect(args.startedArg(2).id).to.eq("square_spec.rb[1:2]") + verify(mockTestRun.started(anything())).times(3) + }) + + test('passed status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:1]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 2", + line: 3 + } + + // Verify that passed status event occurred exactly twice (once as soon as it + // passed and again when parsing the test run output) + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, + expectedTestItem) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, + expectedTestItem) + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Expect failed status events for the other spec in the file + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + + test('failure status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:2]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 3", + line: 7 + } + + // Verify that failed status event occurred twice - once immediately after the + // test failed with no details, and once at the end when parsing test output with + // more information + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + }) + verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", + line: 8, + }) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Expect passed status events for the other spec in the file + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + }) + + suite('single specs from file with passing, skipped and errored specs', async function() { + test('error status event', async function() { + let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:2]")) + + await testRunner.runHandler(errorRequest, cancellationTokenSource.token) + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectedTestItem = { + id: "abs_spec.rb[1:2]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 0", + line: 7, + } + // Verify that failed status occurred immediately and error status event occurred + // when parsing test output with more information + // verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + // testItem: expectedTestItem, + // }) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { + testItem: expectedTestItem, + message: "RuntimeError:\nAbs for zero is not supported", + line: 8, + }) + + // Verify that only expected status events occurred + verify(mockTestRun.started(anything())).times(1) + //verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + + test('skipped event', async function() { + let skippedRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:3]")) + await testRunner.runHandler(skippedRequest, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "abs_spec.rb[1:3]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of -1", + line: 11 + } + testItemMatches(args.startedArg(0), expectation) + testItemMatches(args.skippedArg(0), expectation) + + // Verify that only expected status events occurred + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.skipped(anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + }) + }) }) }); From 3fb967e185198b011afc23ea35e89a53e6a613ca Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 31 Dec 2022 01:39:25 +0000 Subject: [PATCH 049/108] Rearrange RSpec test suite some more This helps ensure more thorough testing of runs both with and without a dry run beforehand --- test/suite/helpers.ts | 9 +- test/suite/rspec/rspec.test.ts | 820 +++++++++++++++++++++------------ 2 files changed, 526 insertions(+), 303 deletions(-) diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 1f6c012..f6f3972 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -85,7 +85,8 @@ export type TestItemExpectation = { label: string, file?: string, line?: number, - children?: TestItemExpectation[] + children?: TestItemExpectation[], + canResolveChildren?: boolean, } /** @@ -121,7 +122,7 @@ export function testItemMatches(testItem: vscode.TestItem, expectation: TestItem if (expectation.children && expectation.children.length > 0) { testItemCollectionMatches(testItem.children, expectation.children, testItem) } - expect(testItem.canResolveChildren).to.be.false + expect(testItem.canResolveChildren).to.eq(expectation.canResolveChildren || false, 'canResolveChildren') expect(testItem.label).to.eq(expectation.label, `label mismatch (id: ${expectation.id})`) expect(testItem.description).to.be.undefined //expect(testItem.description).to.eq(expectation.label, 'description mismatch') @@ -160,10 +161,8 @@ export function testItemCollectionMatches( expectation.length, parent ? `Wrong number of children in item ${parent.id}\n\t${testItems.toString()}` : `Wrong number of items in collection\n\t${testItems.toString()}` ) - let i = 0; testItems.forEach((testItem: vscode.TestItem) => { - testItemMatches(testItem, expectation[i]) - i++ + testItemMatches(testItem, expectation.find(x => x.id == testItem.id)) }) } diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 69ca60c..98117a1 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -28,334 +28,558 @@ suite('Extension Test for RSpec', function() { file) } - this.beforeEach(function () { + this.beforeAll(function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) - testController = new StubTestController() - - // Populate controller with test files. This would be done by the filesystem globs in the watchers - let createTest = (id: string, label?: string) => - testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) - testController.items.add(createTest("abs_spec.rb")) - testController.items.add(createTest("square_spec.rb")) - let subfolderItem = createTest("subfolder") - testController.items.add(subfolderItem) - subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) - config = new RspecConfig(path.resolve("ruby"), workspaceFolder) - - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) - test('Load tests on file resolve request', async function () { - // No tests in suite initially, only test files and folders - testItemCollectionMatches(testController.items, - [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb", - label: "abs_spec.rb", - children: [] - }, - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", - children: [] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", - children: [] - } - ] - }, - ] - ) - - // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) - - // Tests in that file have now been added to suite - testItemCollectionMatches(testController.items, - [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb", - label: "abs_spec.rb", - children: [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - } - ] - }, - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", - children: [] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", - children: [] - } - ] - }, - ] - ) - }) + suite('dry run', function() { + this.beforeEach(function () { + testController = new StubTestController() + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + }) - test('Load all tests', async function () { - await testLoader.discoverAllFilesInWorkspace() - - const testSuite = testController.items - - testItemCollectionMatches(testSuite, - [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb", - label: "abs_spec.rb", - children: [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - } - ] - }, - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", - children: [ - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb[1:1]", - label: "finds the square of 2", - line: 3, - }, - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb[1:2]", - label: "finds the square of 3", - line: 7, - } - ] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb[1:1]", - label: "wibbles and wobbles", - line: 3, - } - ] - } - ] - }, - ] - ) + test('Load tests on file resolve request', async function () { + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string, label?: string) => + testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + testController.items.add(createTest("abs_spec.rb")) + testController.items.add(createTest("square_spec.rb")) + let subfolderItem = createTest("subfolder") + testController.items.add(subfolderItem) + subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) + + // No tests in suite initially, only test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + children: [] + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb", + label: "square_spec.rb", + children: [] + }, + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [] + } + ] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + children: [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + ] + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb", + label: "square_spec.rb", + children: [] + }, + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + children: [] + } + ] + }, + ] + ) + }) + + test('Load all tests', async function () { + await testLoader.discoverAllFilesInWorkspace() + + const testSuite = testController.items + + testItemCollectionMatches(testSuite, + [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb", + label: "abs_spec.rb", + canResolveChildren: true, + children: [ + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + }, + { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + ] + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb", + label: "square_spec.rb", + canResolveChildren: true, + children: [ + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb[1:1]", + label: "finds the square of 2", + line: 3, + }, + { + file: expectedPath("square_spec.rb"), + id: "square_spec.rb[1:2]", + label: "finds the square of 3", + line: 7, + } + ] + }, + { + file: expectedPath("subfolder"), + id: "subfolder", + label: "subfolder", + canResolveChildren: true, + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb", + label: "foo_spec.rb", + canResolveChildren: true, + children: [ + { + file: expectedPath(path.join("subfolder", "foo_spec.rb")), + id: "subfolder/foo_spec.rb[1:1]", + label: "wibbles and wobbles", + line: 3, + } + ] + } + ] + }, + ] + ) + }) }) suite('status events', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() - suite('file with passing and failing specs', function() { - let mockTestRun: vscode.TestRun + suite('dry run before test run', function() { + this.beforeAll(async function () { + testController = new StubTestController() + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + await testLoader.discoverAllFilesInWorkspace() + }) - this.beforeAll(async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square_spec.rb"))) + suite('file with passing and failing specs', function() { + let mockTestRun: vscode.TestRun - let mockRequest = setupMockRequest(testSuite, "square_spec.rb") - let request = instance(mockRequest) - await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - }) + this.beforeAll(async function() { + let mockRequest = setupMockRequest(testSuite, "square_spec.rb") + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() + }) - test('enqueued status event', async function() { - // Not really a useful status event unless you can queue up tests and run only - // parts of the queue at a time - perhaps in the future - verify(mockTestRun.enqueued(anything())).times(0) - }) + test('enqueued status event', async function() { + // Not really a useful status event unless you can queue up tests and run only + // parts of the queue at a time - perhaps in the future + verify(mockTestRun.enqueued(anything())).times(0) + }) - test('started status event', function() { - let args = testStateCaptors(mockTestRun) + test('started status event', function() { + let args = testStateCaptors(mockTestRun) - // Assert that all specs and the file are marked as started - expect(args.startedArg(0).id).to.eq("square_spec.rb") - expect(args.startedArg(1).id).to.eq("square_spec.rb[1:1]") - expect(args.startedArg(2).id).to.eq("square_spec.rb[1:2]") - verify(mockTestRun.started(anything())).times(3) - }) + // Assert that all specs and the file are marked as started + expect(args.startedArg(0).id).to.eq("square_spec.rb") + expect(args.startedArg(1).id).to.eq("square_spec.rb[1:1]") + expect(args.startedArg(2).id).to.eq("square_spec.rb[1:2]") + verify(mockTestRun.started(anything())).times(3) + }) - test('passed status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:1]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 2", - line: 3 - } - - // Verify that passed status event occurred exactly twice (once as soon as it - // passed and again when parsing the test run output) - testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, - expectedTestItem) - testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, - expectedTestItem) - verify(mockTestRun.passed(anything(), anything())).times(2) - - // Expect failed status events for the other spec in the file - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) + test('passed status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:1]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 2", + line: 3 + } + + // Verify that passed status event occurred exactly twice (once as soon as it + // passed and again when parsing the test run output) + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, + expectedTestItem) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, + expectedTestItem) + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Expect failed status events for the other spec in the file + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) - test('failure status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:2]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 3", - line: 7 - } - - // Verify that failed status event occurred twice - once immediately after the - // test failed with no details, and once at the end when parsing test output with - // more information - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, + test('failure status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:2]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 3", + line: 7 + } + + // Verify that failed status event occurred twice - once immediately after the + // test failed with no details, and once at the end when parsing test output with + // more information + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + }) + verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", + line: 8, + }) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Expect passed status events for the other spec in the file + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) }) - verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, - message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", - line: 8, + }) + + suite('single specs from file with passing, skipped and errored specs', async function() { + let mockTestRun: vscode.TestRun + + test('single passing spec', async function() { + let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:1]") + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectedTestItem = { + id: "abs_spec.rb[1:1]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 1", + line: 3 + } + + // Verify that passed status event occurred exactly twice (once as soon as it + // passed and again when parsing the test run output) + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, + expectedTestItem) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, + expectedTestItem) + verify(mockTestRun.passed(anything(), anything())).times(2) + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTestItem.id) + verify(mockTestRun.started(anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) }) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - // Expect passed status events for the other spec in the file - verify(mockTestRun.passed(anything(), anything())).times(2) + test('single spec with error', async function() { + let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:2]") + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectedTestItem = { + id: "abs_spec.rb[1:2]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 0", + line: 7 + } + + // Verify that failed status event occurred twice - once immediately after the + // test failed with no details, and once at the end when parsing test output with + // more information + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + }) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { + testItem: expectedTestItem, + message: "RuntimeError:\nAbs for zero is not supported", + line: 8, + }) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTestItem.id) + verify(mockTestRun.started(anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) + test('single skipped spec', async function() { + let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:3]") + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "abs_spec.rb[1:3]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of -1", + line: 11 + } + testItemMatches(args.skippedArg(0), expectation) + + // Verify that only expected status events occurred + verify(mockTestRun.enqueued(anything())).times(1) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.skipped(anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + }) }) }) - suite('single specs from file with passing, skipped and errored specs', async function() { - test('error status event', async function() { - let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:2]")) - - await testRunner.runHandler(errorRequest, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let expectedTestItem = { - id: "abs_spec.rb[1:2]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 0", - line: 7, - } - // Verify that failed status occurred immediately and error status event occurred - // when parsing test output with more information - // verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - // testItem: expectedTestItem, - // }) - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { - testItem: expectedTestItem, - message: "RuntimeError:\nAbs for zero is not supported", - line: 8, + suite('test run without dry run', function () { + this.beforeAll(function () { + testController = new StubTestController() + testSuite = new TestSuite(stdout_logger(), testController, config) + testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + }) + + suite('file with passing and failing specs', function() { + let mockTestRun: vscode.TestRun + + this.beforeAll(async function() { + let request = instance(setupMockRequest(testSuite, "square_spec.rb")) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun() }) - // Verify that only expected status events occurred - verify(mockTestRun.started(anything())).times(1) - //verify(mockTestRun.failed(anything(), anything(), anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + test('enqueued status event', async function() { + // Not really a useful status event unless you can queue up tests and run only + // parts of the queue at a time - perhaps in the future + verify(mockTestRun.enqueued(anything())).times(0) + }) - // Verify that no other status events occurred - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) + test('started status event', function() { + let args = testStateCaptors(mockTestRun) + + // Assert that all specs and the file are marked as started + expect(args.startedArg(0).id).to.eq("square_spec.rb") + verify(mockTestRun.started(anything())).times(1) + }) + + test('passed status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:1]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 2", + line: 3 + } + + // Verify that passed status event occurred exactly twice (once as soon as it + // passed and again when parsing the test run output) + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, + expectedTestItem) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, + expectedTestItem) + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Expect failed status events for the other spec in the file + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + + test('failure status event', function() { + let expectedTestItem = { + id: "square_spec.rb[1:2]", + file: expectedPath("square_spec.rb"), + label: "finds the square of 3", + line: 7 + } + + // Verify that failed status event occurred twice - once immediately after the + // test failed with no details, and once at the end when parsing test output with + // more information + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + }) + verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { + testItem: expectedTestItem, + message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", + line: 8, + }) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + + // Expect passed status events for the other spec in the file + verify(mockTestRun.passed(anything(), anything())).times(2) + + // Verify that no other status events occurred + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) }) - test('skipped event', async function() { - let skippedRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:3]")) - await testRunner.runHandler(skippedRequest, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun) - let expectation = { - id: "abs_spec.rb[1:3]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of -1", - line: 11 - } - testItemMatches(args.startedArg(0), expectation) - testItemMatches(args.skippedArg(0), expectation) - - // Verify that only expected status events occurred - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.skipped(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + suite('single specs from file with passing, skipped and errored specs', async function() { + test('passing spec', async function() { + let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:1]")) + + await testRunner.runHandler(errorRequest, cancellationTokenSource.token) + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectedTestItem = { + id: "abs_spec.rb[1:1]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 1", + line: 3, + } + // Verify that passed status event occurred exactly once (once as soon as it + // passed and again when parsing the test run output) + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, + expectedTestItem) + + // Verify that only expected status events occurred + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.passed(anything(), anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + + test('errored spec', async function() { + let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:2]")) + + await testRunner.runHandler(errorRequest, cancellationTokenSource.token) + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let expectedTestItem = { + id: "abs_spec.rb[1:2]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of 0", + line: 7, + } + // Verify that failed status occurred immediately and error status event occurred + // when parsing test output with more information + // verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { + // testItem: expectedTestItem, + // }) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { + testItem: expectedTestItem, + message: "RuntimeError:\nAbs for zero is not supported", + line: 8, + }) + + // Verify that only expected status events occurred + verify(mockTestRun.started(anything())).times(1) + //verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + }) + + test('skipped spec', async function() { + let skippedRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:3]")) + await testRunner.runHandler(skippedRequest, cancellationTokenSource.token) + + let mockTestRun = (testController as StubTestController).getMockTestRun() + + let args = testStateCaptors(mockTestRun) + let expectation = { + id: "abs_spec.rb[1:3]", + file: expectedPath("abs_spec.rb"), + label: "finds the absolute value of -1", + line: 11 + } + testItemMatches(args.startedArg(0), expectation) + testItemMatches(args.skippedArg(0), expectation) + + // Verify that only expected status events occurred + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.skipped(anything())).times(1) + + // Verify that no other status events occurred + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + }) }) }) }) From 13930e718cc18774327d6c33282a3fa1b59e592a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 2 Jan 2023 19:59:07 +0000 Subject: [PATCH 050/108] Finish merging status handling into TestRunner and fix behaviour --- src/rspec/rspecTestRunner.ts | 7 +- src/testLoader.ts | 133 +---- src/testRunContext.ts | 6 +- src/testRunner.ts | 121 ++-- src/testSuite.ts | 92 +-- .../rspec/spec/{ => square}/square_spec.rb | 0 .../fixtures/rspec/spec/subfolder/foo_spec.rb | 7 - test/stubs/stubTestController.ts | 28 +- test/stubs/stubTestItem.ts | 5 +- test/stubs/stubTestItemCollection.ts | 22 + test/suite/helpers.ts | 121 ++-- test/suite/minitest/minitest.test.ts | 18 +- test/suite/rspec/rspec.test.ts | 523 ++++++------------ test/suite/unitTests/testLoader.test.ts | 243 -------- test/suite/unitTests/testRunner.test.ts | 250 +++++++++ test/suite/unitTests/testSuite.test.ts | 32 +- 16 files changed, 703 insertions(+), 905 deletions(-) rename test/fixtures/rspec/spec/{ => square}/square_spec.rb (100%) delete mode 100644 test/fixtures/rspec/spec/subfolder/foo_spec.rb delete mode 100644 test/suite/unitTests/testLoader.test.ts create mode 100644 test/suite/unitTests/testRunner.test.ts diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 3c4ff95..e229eb7 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -71,12 +71,13 @@ export class RspecTestRunner extends TestRunner { */ handleStatus(test: ParsedTest, context: TestRunContext): void { let log = this.log.getChildLogger({ label: "handleStatus" }) - log.debug(`Handling status of test: ${JSON.stringify(test)}`); + log.trace(`Handling status of test: ${JSON.stringify(test)}`); let testItem = this.testSuite.getOrCreateTestItem(test.id) if (test.status === "passed") { - log.debug("Passed", testItem.id) + log.trace("Passed", testItem.id) context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { + log.trace("Failed/Errored", testItem.id) // Remove linebreaks from error message. let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); // Prepend the class name to the error message string. @@ -123,7 +124,7 @@ export class RspecTestRunner extends TestRunner { } } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { // Handle pending test cases. - log.debug("Skipped", testItem.id) + log.trace("Skipped", testItem.id) context.skipped(testItem) } }; diff --git a/src/testLoader.ts b/src/testLoader.ts index 7b0cf77..e2a58f9 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,7 +1,5 @@ import * as vscode from 'vscode'; -import * as path from 'path'; import { IChildLogger } from '@vscode-logging/logger'; -import { TestRunner } from './testRunner'; import { RspecTestRunner } from './rspec/rspecTestRunner'; import { MinitestTestRunner } from './minitest/minitestTestRunner'; import { Config } from './config'; @@ -120,142 +118,13 @@ export class TestLoader implements vscode.Disposable { let output = await this.testRunner.initTests(testItems); log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); - output = TestRunner.getJsonFromOutput(output); - log.debug(`Parsing the returned JSON: ${output}`); - let testMetadata; - try { - testMetadata = JSON.parse(output); - } catch (error) { - log.error('JSON parsing failed', error); - } - - let tests = TestLoader.parseDryRunOutput( - this.log, - this.testSuite, - testMetadata.examples - ) - - log.debug("Test output parsed. Adding tests to test suite", tests) - testItems.forEach((testItem) => { - // TODO: Add option to list only tests for single file to minitest and remove filter below - log.debug(`testItem fsPath: ${testItem.uri?.fsPath}`) - var filteredTests = tests.filter((test) => { - log.debug(`filter: test file path: ${test.file_path}`) - return testItem.uri?.fsPath.endsWith(test.file_path) - }) - this.getTestSuiteForFile(filteredTests, testItem); - }) + this.testRunner.parseAndHandleTestOutput(output) } catch (e: any) { log.error("Failed to load tests", e) return Promise.reject(e) } } - public static parseDryRunOutput( - rootLog: IChildLogger, - testSuite: TestSuite, - tests: ParsedTest[] - ): ParsedTest[] { - let log = rootLog.getChildLogger({ label: "parseDryRunOutput" }) - log.debug(`called with ${tests.length} items`) - let parsedTests: Array = []; - - tests.forEach( - (test: ParsedTest) => { - log.debug("Parsing test", test) - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let test_location_string: string = test_location_array.join(''); - let location = parseInt(test_location_string); - let id = testSuite.normaliseTestId(test.id) - let file_path = TestLoader.normaliseFilePath(testSuite, test.file_path) - let parsedTest = { - ...test, - id: id, - file_path: file_path, - location: location, - } - parsedTests.push(parsedTest); - log.debug("Parsed test", parsedTest) - } - ); - return parsedTests - } - - public static normaliseFilePath(testSuite: TestSuite, filePath: string): string { - filePath = testSuite.normaliseTestId(filePath) - return filePath.replace(/\[.*/, '') - } - - /** - * Get the tests in a given file. - * - * @param tests Parsed output from framework - * @param testItem TestItem object containing file details - */ - public getTestSuiteForFile(tests: Array, testItem: vscode.TestItem) { - let log = this.log.getChildLogger({ label: `getTestSuiteForFile(${testItem.id})` }) - - let currentFileSplitName = testItem.uri?.fsPath.split(path.sep); - let currentFileLabel = currentFileSplitName ? currentFileSplitName[currentFileSplitName!.length - 1] : testItem.label - log.debug(`Current file label: ${currentFileLabel}`) - - let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - - let testItems: vscode.TestItem[] = [] - tests.forEach((test) => { - log.debug(`Building test: ${test.id}`) - // RSpec provides test ids like "file_name.rb[1:2:3]". - // This uses the digits at the end of the id to create - // an array of numbers representing the location of the - // test in the file. - let testLocationArray: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').map((x) => { - return parseInt(x); - }); - - // Get the last element in the location array. - let testNumber: number = testLocationArray[testLocationArray.length - 1]; - // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" - // is appended to the test description to distinguish between separate tests. - let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; - - // If the current file label doesn't have a slash in it and it starts with the PascalCase'd - // file name, remove the from the start of the description. This turns, e.g. - // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. - if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { - // Optional check for a space following the PascalCase file name. In some - // cases, e.g. 'FileName#method_name` there's no space after the file name. - let regexString = `${pascalCurrentFileLabel}[ ]?`; - let regex = new RegExp(regexString, "g"); - description = description.replace(regex, ''); - } - - let childTestItem = this.testSuite.getOrCreateTestItem(test.id) - childTestItem.canResolveChildren = false - log.debug(`Setting test ${childTestItem.id} label to "${description}"`) - childTestItem.label = description - childTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); - - testItems.push(childTestItem); - }); - testItem.children.replace(testItems) - } - - - - /** - * Convert a string from snake_case to PascalCase. - * Note that the function will return the input string unchanged if it - * includes a '/'. - * - * @param string The string to convert to PascalCase. - * @return The converted string. - */ - private snakeToPascalCase(string: string): string { - if (string.includes('/')) { return string } - return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); - } - - public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { let log = this.log.getChildLogger({label: "parseTestsInFile"}) let testItem: vscode.TestItem diff --git a/src/testRunContext.ts b/src/testRunContext.ts index f5913ad..9ff78ab 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -66,7 +66,8 @@ export class TestRunContext { new vscode.Position(line, 0) ) this.testRun.errored(testItem, testMessage, duration) - this.log.debug(`Errored: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) + this.log.debug(`Errored: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + this.log.trace(`Error message: ${message}`) } catch (e: any) { this.log.error(`Failed to set test ${test} as Errored`, e) } @@ -94,7 +95,8 @@ export class TestRunContext { new vscode.Position(line, 0) ) this.testRun.failed(test, testMessage, duration) - this.log.debug(`Failed: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''} - ${message}`) + this.log.debug(`Failed: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) + this.log.trace(`Failure message: ${message}`) } /** diff --git a/src/testRunner.ts b/src/testRunner.ts index c767ae6..2ccd9fd 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import * as childProcess from 'child_process'; import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; @@ -134,7 +135,7 @@ export abstract class TestRunner implements vscode.Disposable { process.stderr!.pipe(split2()).on('data', (data) => { data = data.toString(); - log.debug(data); + log.trace(data); if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { this.debugCommandStartedResolver() } @@ -142,7 +143,7 @@ export abstract class TestRunner implements vscode.Disposable { process.stdout!.pipe(split2()).on('data', (data) => { data = data.toString(); - log.debug(data); + log.trace(data); let getTest = (testId: string): vscode.TestItem => { testId = this.testSuite.normaliseTestId(testId) return this.testSuite.getOrCreateTestItem(testId) @@ -153,13 +154,14 @@ export abstract class TestRunner implements vscode.Disposable { } else if (data.startsWith('FAILED:')) { log.debug(`Received test status - FAILED`, data) let testItem = getTest(data.replace('FAILED: ', '')) - context.failed(testItem, "", testItem.uri?.fsPath || "", testItem.range?.start.line || 0) + let line = testItem.range?.start?.line ? testItem.range.start.line + 1 : 0 + context.failed(testItem, "", testItem.uri?.fsPath || "", line) } else if (data.startsWith('RUNNING:')) { log.debug(`Received test status - RUNNING`, data) context.started(getTest(data.replace('RUNNING: ', ''))) } else if (data.startsWith('PENDING:')) { log.debug(`Received test status - PENDING`, data) - context.enqueued(getTest(data.replace('PENDING: ', ''))) + context.skipped(getTest(data.replace('PENDING: ', ''))) } if (data.includes('START_OF_TEST_JSON')) { buffer = data; @@ -196,7 +198,7 @@ export abstract class TestRunner implements vscode.Disposable { const queue: vscode.TestItem[] = []; if (debuggerConfig) { - log.info(`Debugging test(s) ${JSON.stringify(request.include)}`); + log.debug(`Debugging test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); if (!this.workspace) { log.error("Cannot debug without a folder opened") @@ -223,38 +225,32 @@ export abstract class TestRunner implements vscode.Disposable { }); } else { - log.info(`Running test(s) ${JSON.stringify(request.include)}`); + log.debug(`Running test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); } // Loop through all included tests, or all known tests, and add them to our queue if (request.include) { - log.debug(`${request.include.length} tests in request`); + log.trace(`${request.include.length} tests in request`); request.include.forEach(test => queue.push(test)); // For every test that was queued, try to run it while (queue.length > 0 && !token.isCancellationRequested) { const test = queue.pop()!; - log.debug(`Running test from queue ${test.id}`); + log.trace(`Running test from queue ${test.id}`); // Skip tests the user asked to exclude if (request.exclude?.includes(test)) { + log.debug(`Skipping test excluded from test run: ${test.id}`) continue; } await this.runNode(test, context); - - test.children.forEach(test => { - if (test.id.endsWith('.rb')) { - // Only add files, not all the single test cases - queue.push(test) - } - }); } if (token.isCancellationRequested) { - log.debug(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } else { - log.debug('Running all tests in suite'); + log.trace('Running all tests in suite'); await this.runNode(null, context); } } @@ -318,36 +314,31 @@ export abstract class TestRunner implements vscode.Disposable { if (node == null) { log.debug("Running all tests") this.controller.items.forEach((testSuite) => { - //this.enqueTestAndChildren(testSuite, context) // Mark selected tests as started this.markTestAndChildrenStarted(testSuite, context) }) let testOutput = await this.runFullTestSuite(context); - this.parseAndHandleTestOutput(testOutput, context) + this.parseAndHandleTestOutput(testOutput, context, undefined) // If the suite is a file, run the tests as a file rather than as separate tests. - } else if (node.label.endsWith('.rb')) { - log.debug(`Running test file: ${node.id}`) - - // Mark selected tests as enqueued - not really much point - //this.enqueTestAndChildren(node, context) + } else if (node.canResolveChildren) { + log.debug(`Running test file/folder: ${node.id}`) // Mark selected tests as started this.markTestAndChildrenStarted(node, context) let testOutput = await this.runTestFile(node, context); - this.parseAndHandleTestOutput(testOutput, context) + this.parseAndHandleTestOutput(testOutput, context, node) } else { - log.debug(`Running single test: ${node.id}`) if (node.uri !== undefined) { log.debug(`Running single test: ${node.id}`) - context.started(node) + this.markTestAndChildrenStarted(node, context) // Run the test at the given line, add one since the line is 0-indexed in // VS Code and 1-indexed for RSpec/Minitest. let testOutput = await this.runSingleTest(node, context); - this.parseAndHandleTestOutput(testOutput, context) + this.parseAndHandleTestOutput(testOutput, context, node) } else { log.error("test missing file path") } @@ -357,20 +348,82 @@ export abstract class TestRunner implements vscode.Disposable { } } - private parseAndHandleTestOutput(testOutput: string, context: TestRunContext) { + public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext, testItem?: vscode.TestItem) { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); - log.debug('Parsing the below JSON:'); - log.debug(`${testOutput}`); + log.trace('Parsing the below JSON:'); + log.trace(`${testOutput}`); let testMetadata = JSON.parse(testOutput); let tests: Array = testMetadata.examples; + let parsedTests: vscode.TestItem[] = [] if (tests && tests.length > 0) { tests.forEach((test: ParsedTest) => { test.id = this.testSuite.normaliseTestId(test.id) - this.handleStatus(test, context); + + // RSpec provides test ids like "file_name.rb[1:2:3]". + // This uses the digits at the end of the id to create + // an array of numbers representing the location of the + // test in the file. + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let testNumber = test_location_array[test_location_array.length - 1]; + test.file_path = this.testSuite.normaliseTestId(test.file_path).replace(/\[.*/, '') + let currentFileLabel = test.file_path.split(path.sep).slice(-1)[0] + let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); + // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" + // is appended to the test description to distinguish between separate tests. + let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; + + // If the current file label doesn't have a slash in it and it starts with the PascalCase'd + // file name, remove the from the start of the description. This turns, e.g. + // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. + if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { + // Optional check for a space following the PascalCase file name. In some + // cases, e.g. 'FileName#method_name` there's no space after the file name. + let regexString = `${pascalCurrentFileLabel}[ ]?`; + let regex = new RegExp(regexString, "g"); + description = description.replace(regex, ''); + } + test.description = description + let test_location_string: string = test_location_array.join(''); + test.location = parseInt(test_location_string); + + let newTestItem = this.testSuite.getOrCreateTestItem(test.id) + newTestItem.canResolveChildren = !test.id.endsWith(']') + log.trace(`canResolveChildren (${test.id}): ${newTestItem.canResolveChildren}`) + log.trace(`Setting test ${newTestItem.id} label to "${description}"`) + newTestItem.label = description + newTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); + parsedTests.push(newTestItem) + let parent = newTestItem.parent + while (parent) { + if (!parsedTests.includes(parent)) { + parsedTests.push(parent) + } + parent = parent.parent + } + log.trace("Parsed test", test) + if(context) { + // Only handle status if actual test run, not dry run + this.handleStatus(test, context); + } }); } + this.testSuite.removeMissingTests(parsedTests, testItem) + + } + + /** + * Convert a string from snake_case to PascalCase. + * Note that the function will return the input string unchanged if it + * includes a '/'. + * + * @param string The string to convert to PascalCase. + * @return The converted string. + */ + private snakeToPascalCase(string: string): string { + if (string.includes('/')) { return string } + return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); } public async debugCommandStarted(): Promise { @@ -409,7 +462,7 @@ export abstract class TestRunner implements vscode.Disposable { * @return The raw output from running the test suite. */ async runTestFramework(testCommand: string, type: string, context: TestRunContext): Promise { - this.log.info(`Running test suite: ${type}`); + this.log.trace(`Running test suite: ${type}`); return await this.spawnCancellableChild(testCommand, context) }; @@ -434,7 +487,7 @@ export abstract class TestRunner implements vscode.Disposable { env: context.config.getProcessEnv() }; - this.log.info(`Running command: ${testCommand}`); + this.log.debug(`Running command: ${testCommand}`); let testProcess = childProcess.spawn( testCommand, diff --git a/src/testSuite.ts b/src/testSuite.ts index 1e8a5d0..b11b5f2 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -24,15 +24,20 @@ export class TestSuite { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.uriToTestId(testId) log.debug(`Deleting test ${testId}`) - let collection = this.getParentTestItemCollection(testId, false) - if (!collection) { - log.debug('Parent test collection not found') - return + let parent = this.getOrCreateParent(testId, false) + let collection: vscode.TestItemCollection | undefined + if (!parent) { + log.debug('Parent is controller') + collection = this.controller.items + } else { + log.debug(`Parent is ${parent.id}`) + collection = parent.children } - let testItem = collection.get(testId) - if (testItem) { - collection.delete(testItem.id); - log.debug(`Removed test ${testItem.id}`) + if (collection) { + collection.delete(testId); + log.debug(`Removed test ${testId}`) + } else { + log.error("Collection not found") } } @@ -49,8 +54,8 @@ export class TestSuite { testId = testId.substring(2) } log.debug(`Looking for test ${testId}`) - let collection = this.getParentTestItemCollection(testId, true)! - let testItem = collection.get(testId) + let parent = this.getOrCreateParent(testId, true) + let testItem = (parent?.children || this.controller.items).get(testId) if (!testItem) { // Create a basic test item with what little info we have to be filled in later let label = testId.substring(testId.lastIndexOf(path.sep) + 1) @@ -58,9 +63,9 @@ export class TestSuite { label = this.getPlaceholderLabelForSingleTest(testId) } testItem = this.createTestItem( - collection, testId, label, + parent, !this.locationPattern.test(testId) ); } @@ -75,8 +80,8 @@ export class TestSuite { public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: 'getTestItem'}) testId = this.uriToTestId(testId) - let collection = this.getParentTestItemCollection(testId, false) - let testItem = collection?.get(testId) + let parent = this.getOrCreateParent(testId, false) + let testItem = (parent?.children || this.controller.items).get(testId) if (!testItem) { log.debug(`Couldn't find ${testId}`) return undefined @@ -84,6 +89,26 @@ export class TestSuite { return testItem } + public removeMissingTests(parsedTests: vscode.TestItem[], parent?: vscode.TestItem) { + //let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) + let collection = parent?.children || this.controller.items + collection.forEach(item => { + if (!item.canResolveChildren) { + //log.debug(`Not an item with children - returning`) + return + } + //log.debug(`Checking tests in ${item.id}`) + if (parsedTests.find(x => x.id == item.id)) { + //log.debug(`Parsed test contains ${item.id}`) + let filteredTests = parsedTests.filter(x => x.parent?.id == item.id) + this.removeMissingTests(filteredTests, item) + } else { + //log.debug(`Parsed tests don't contain ${item.id}. Deleting`) + collection.delete(item.id) + } + }) + } + /** * Takes a test ID from the test runner output and normalises it to a consistent format * @@ -133,26 +158,24 @@ export class TestSuite { * @param createIfMissing Create parent test collections if missing * @returns Parent collection of the given test ID */ - private getParentTestItemCollection(testId: string, createIfMissing: boolean): vscode.TestItemCollection | undefined { - let log = this.log.getChildLogger({label: `getParentTestItemCollection(${testId}, createIfMissing: ${createIfMissing})`}) + private getOrCreateParent(testId: string, createIfMissing: boolean): vscode.TestItem | undefined { + let log = this.log.getChildLogger({label: `${this.getOrCreateParent.name}(${testId}, createIfMissing: ${createIfMissing})`}) let idSegments = this.splitTestId(testId) - let collection: vscode.TestItemCollection = this.controller.items + let parent: vscode.TestItem | undefined // Walk through test folders to find the collection containing our test file for (let i = 0; i < idSegments.length - 1; i++) { let collectionId = this.getPartialId(idSegments, i) log.debug(`Getting parent collection ${collectionId}`) - let childCollection = collection.get(collectionId)?.children - if (!childCollection) { + let child = this.controller.items.get(collectionId) + if (!child) { if (!createIfMissing) return undefined - let child = this.createTestItem( - collection, + child = this.createTestItem( collectionId, idSegments[i] ) - childCollection = child.children } - collection = childCollection + parent = child } // TODO: This might not handle nested describe/context/etc blocks? @@ -163,22 +186,21 @@ export class TestSuite { if (fileId.startsWith(path.sep)) { fileId = fileId.substring(1) } - let childCollection = collection.get(fileId)?.children - if (!childCollection) { + let child = (parent?.children || this.controller.items).get(fileId) + if (!child) { log.debug(`TestItem for file ${fileId} not in parent collection`) if (!createIfMissing) return undefined - let child = this.createTestItem( - collection, + child = this.createTestItem( fileId, - fileId.substring(fileId.lastIndexOf(path.sep) + 1) + fileId.substring(fileId.lastIndexOf(path.sep) + 1), + parent, ) - childCollection = child.children } log.debug(`Got TestItem for file ${fileId} from parent collection`) - collection = childCollection + parent = child } // else test item is the file so return the file's parent - return collection + return parent } /** @@ -191,17 +213,17 @@ export class TestSuite { * @returns */ private createTestItem( - collection: vscode.TestItemCollection, testId: string, label: string, + parent?: vscode.TestItem, canResolveChildren: boolean = true ): vscode.TestItem { - let log = this.log.getChildLogger({ label: `createTestId(${testId})` }) + let log = this.log.getChildLogger({ label: `${this.createTestItem.name}(${testId})` }) let uri = this.testIdToUri(testId) - log.debug(`Creating test item - label: ${label}, uri: ${uri}, canResolveChildren: ${canResolveChildren}`) + log.debug(`Creating test item - label: ${label}, parent: ${parent?.id}, canResolveChildren: ${canResolveChildren}, uri: ${uri},`) let item = this.controller.createTestItem(testId, label, uri) - item.canResolveChildren = canResolveChildren - collection.add(item); + item.canResolveChildren = canResolveChildren; + (parent?.children || this.controller.items).add(item); log.debug(`Added test ${item.id}`) return item } diff --git a/test/fixtures/rspec/spec/square_spec.rb b/test/fixtures/rspec/spec/square/square_spec.rb similarity index 100% rename from test/fixtures/rspec/spec/square_spec.rb rename to test/fixtures/rspec/spec/square/square_spec.rb diff --git a/test/fixtures/rspec/spec/subfolder/foo_spec.rb b/test/fixtures/rspec/spec/subfolder/foo_spec.rb deleted file mode 100644 index 779094a..0000000 --- a/test/fixtures/rspec/spec/subfolder/foo_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "test_helper" - -describe "Foo" do - it "wibbles and wobbles" do - expect("foo").to.not eq("bar") - end -end diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts index 11ae447..9b15b2b 100644 --- a/test/stubs/stubTestController.ts +++ b/test/stubs/stubTestController.ts @@ -3,12 +3,19 @@ import { instance, mock } from 'ts-mockito'; import { StubTestItemCollection } from './stubTestItemCollection'; import { StubTestItem } from './stubTestItem'; +import { IChildLogger } from '@vscode-logging/logger'; export class StubTestController implements vscode.TestController { id: string = "stub_test_controller_id"; label: string = "stub_test_controller_label"; - items: vscode.TestItemCollection = new StubTestItemCollection(); - mockTestRun: vscode.TestRun | undefined + items: vscode.TestItemCollection + testRuns: Map = new Map() + readonly rootLog: IChildLogger + + constructor(readonly log: IChildLogger) { + this.rootLog = log + this.items = new StubTestItemCollection(log, this); + } createRunProfile( label: string, @@ -29,20 +36,19 @@ export class StubTestController implements vscode.TestController { name?: string, persist?: boolean ): vscode.TestRun { - this.mockTestRun = mock() - return instance(this.mockTestRun) + let mockTestRun = mock() + this.testRuns.set(request, mockTestRun) + return instance(mockTestRun) } createTestItem(id: string, label: string, uri?: vscode.Uri): vscode.TestItem { - return new StubTestItem(id, label, uri) + return new StubTestItem(this.rootLog, this, id, label, uri) } - dispose = () => {} - - getMockTestRun(): vscode.TestRun { - if (this.mockTestRun) - return this.mockTestRun - throw new Error("No test run") + getMockTestRun(request: vscode.TestRunRequest): vscode.TestRun | undefined { + return this.testRuns.get(request) } + dispose = () => {} + } \ No newline at end of file diff --git a/test/stubs/stubTestItem.ts b/test/stubs/stubTestItem.ts index be89199..c6205e0 100644 --- a/test/stubs/stubTestItem.ts +++ b/test/stubs/stubTestItem.ts @@ -1,3 +1,4 @@ +import { IChildLogger } from '@vscode-logging/logger'; import * as vscode from 'vscode' import { StubTestItemCollection } from './stubTestItemCollection'; @@ -15,11 +16,11 @@ export class StubTestItem implements vscode.TestItem { range: vscode.Range | undefined; error: string | vscode.MarkdownString | undefined; - constructor(id: string, label: string, uri?: vscode.Uri) { + constructor(rootLog: IChildLogger, controller: vscode.TestController, id: string, label: string, uri?: vscode.Uri) { this.id = id this.label = label this.uri = uri - this.children = new StubTestItemCollection() + this.children = new StubTestItemCollection(rootLog, controller, this) this.tags = [] this.canResolveChildren = false this.busy = false diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index 8eda3eb..f3612a4 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -1,10 +1,20 @@ +import { IChildLogger } from '@vscode-logging/logger'; import * as vscode from 'vscode' +import { StubTestItem } from './stubTestItem'; export class StubTestItemCollection implements vscode.TestItemCollection { private testIds: { [name: string]: vscode.TestItem } = {}; get size(): number { return Object.keys(this.testIds).length; }; + private readonly log: IChildLogger + private readonly parentItem?: vscode.TestItem; + + constructor(readonly rootLog: IChildLogger, controller: vscode.TestController, parent?: vscode.TestItem) { + this.log = rootLog.getChildLogger({label: `StubTestItemCollection(${parent?.id || 'controller'})`}) + this.parentItem = parent + } replace(items: readonly vscode.TestItem[]): void { + //this.log.debug(`Replacing all tests`, JSON.stringify(Object.keys(this.testIds)), JSON.stringify(items.map(x => x.id))) this.testIds = {} items.forEach(item => { this.testIds[item.id] = item @@ -37,10 +47,22 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } add(item: vscode.TestItem): void { + this.log.debug(`Adding test ${item.id} to ${JSON.stringify(Object.keys(this.testIds))}`) this.testIds[item.id] = item + let sortedIds = Object.values(this.testIds).sort((a, b) => { + if(a.id > b.id) return 1 + if(a.id < b.id) return -1 + return 0 + }) + this.testIds = {} + sortedIds.forEach(item => this.testIds[item.id] = item) + if (this.parentItem) { + (item as StubTestItem).parent = this.parentItem + } } delete(itemId: string): void { + this.log.debug(`Deleting test ${itemId} from ${JSON.stringify(Object.keys(this.testIds))}`) delete this.testIds[itemId] } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index f6f3972..1e10734 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode' import { expect } from 'chai' -import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; +import { IVSCodeExtLogger, IChildLogger, LogLevel } from "@vscode-logging/types"; import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; @@ -27,14 +27,6 @@ const NOOP_LOGGER: IVSCodeExtLogger = { } Object.freeze(NOOP_LOGGER) -function writeStdOutLogMsg(level: string, msg: string, ...args: any[]): void { - console.log(`[${level}] ${msg}${args.length > 0 ? ':' : ''}`) - args.forEach((arg) => { - console.log(`${JSON.stringify(arg)}`) - }) - console.log('----------') -} - function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogger { let prependLabel = (l:string, m:string):string => `${l}: ${m}` return { @@ -48,34 +40,50 @@ function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogge } } -/** - * Logger that logs to stdout - not terribly pretty but useful for seeing what failing tests are doing - */ -const STDOUT_LOGGER: IVSCodeExtLogger = { - changeLevel: noop, - changeSourceLocationTracking: noop, - debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, - error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, - fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, - getChildLogger(opts: { label: string }): IChildLogger { - return createChildLogger(this, opts.label); - }, - info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, - trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, - warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } -} -Object.freeze(STDOUT_LOGGER) - /** * Get a noop logger for use in testing where logs are usually unnecessary */ export function noop_logger(): IVSCodeExtLogger { return NOOP_LOGGER } + /** * Get a logger that logs to stdout. * * Not terribly pretty but useful for seeing what failing tests are doing */ -export function stdout_logger(): IVSCodeExtLogger { return STDOUT_LOGGER } +export function stdout_logger(level: LogLevel = "info"): IVSCodeExtLogger { + const levels: { [key: string]: number } = { + "fatal": 0, + "error": 1, + "warn": 2, + "info": 3, + "debug": 4, + "trace": 5, + } + let maxLevel = levels[level] + function writeStdOutLogMsg(level: LogLevel, msg: string, ...args: any[]): void { + if (levels[level] <= maxLevel) { + console.log(`[${level}] ${msg}${args.length > 0 ? ':' : ''}`) + args.forEach((arg) => { + console.log(`${JSON.stringify(arg)}`) + }) + console.log('----------') + } + } + let logger: IVSCodeExtLogger = { + changeLevel: (level: LogLevel) => { maxLevel = levels[level] }, + changeSourceLocationTracking: noop, + debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, + error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, + fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, + getChildLogger(opts: { label: string }): IChildLogger { + return createChildLogger(this, opts.label); + }, + info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, + trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, + warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } + } + return logger +} /** * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values @@ -93,7 +101,6 @@ export type TestItemExpectation = { * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values */ export type TestFailureExpectation = { - testItem: TestItemExpectation, message?: string, actualOutput?: string, expectedOutput?: string, @@ -114,21 +121,21 @@ export function testUriMatches(testItem: vscode.TestItem, path?: string) { * @param testItem {@link vscode.TestItem TestItem} to check * @param expectation {@link TestItemExpectation} to check against */ -export function testItemMatches(testItem: vscode.TestItem, expectation: TestItemExpectation | undefined) { +export function testItemMatches(testItem: vscode.TestItem, expectation?: TestItemExpectation, message?: string) { if (!expectation) expect.fail("No expectation given") - expect(testItem.id).to.eq(expectation.id, `id mismatch (expected: ${expectation.id})`) + expect(testItem.id).to.eq(expectation.id, `${message ? message + ' - ' : ''}id mismatch (expected: ${expectation.id})`) testUriMatches(testItem, expectation.file) if (expectation.children && expectation.children.length > 0) { testItemCollectionMatches(testItem.children, expectation.children, testItem) } - expect(testItem.canResolveChildren).to.eq(expectation.canResolveChildren || false, 'canResolveChildren') - expect(testItem.label).to.eq(expectation.label, `label mismatch (id: ${expectation.id})`) + expect(testItem.canResolveChildren).to.eq(expectation.canResolveChildren || false, `${message ? message + ' - ' : ''}canResolveChildren (id: ${expectation.id})`) + expect(testItem.label).to.eq(expectation.label, `${message ? message + ' - ' : ''}label mismatch (id: ${expectation.id})`) expect(testItem.description).to.be.undefined //expect(testItem.description).to.eq(expectation.label, 'description mismatch') if (expectation.line) { expect(testItem.range).to.not.be.undefined - expect(testItem.range?.start.line).to.eq(expectation.line, `line number mismatch (id: ${expectation.id})`) + expect(testItem.range?.start.line).to.eq(expectation.line, `${message ? message + ' - ' : ''}line number mismatch (id: ${expectation.id})`) } else { expect(testItem.range).to.be.undefined } @@ -162,31 +169,38 @@ export function testItemCollectionMatches( parent ? `Wrong number of children in item ${parent.id}\n\t${testItems.toString()}` : `Wrong number of items in collection\n\t${testItems.toString()}` ) testItems.forEach((testItem: vscode.TestItem) => { - testItemMatches(testItem, expectation.find(x => x.id == testItem.id)) + let expectedItem = expectation.find(x => x.id == testItem.id) + if(!expectedItem) { + expect.fail(`${testItem.id} not found in expected items`) + } + testItemMatches(testItem, expectedItem) }) } export function verifyFailure( index: number, captor: ArgCaptor3, - expectation: TestFailureExpectation): void + expectedTestItem: TestItemExpectation, + expectation: TestFailureExpectation, + message?: string): void { let failure = captor.byCallIndex(index) let testItem = failure[0] let failureDetails = failure[1] - testItemMatches(testItem, expectation.testItem) + let messagePrefix = message ? `${message} - ${testItem.id}` : testItem.id + testItemMatches(testItem, expectedTestItem) if (expectation.message) { - expect(failureDetails.message).to.contain(expectation.message) + expect(failureDetails.message).to.contain(expectation.message, `${messagePrefix}: message`) } else { expect(failureDetails.message).to.eq('') } - expect(failureDetails.actualOutput).to.eq(expectation.actualOutput) - expect(failureDetails.expectedOutput).to.eq(expectation.expectedOutput) - expect(failureDetails.location?.range.start.line).to.eq(expectation.line || expectation.testItem.line || 0) - expect(failureDetails.location?.uri.fsPath).to.eq(expectation.testItem.file) + expect(failureDetails.actualOutput).to.eq(expectation.actualOutput, `${messagePrefix}: actualOutput`) + expect(failureDetails.expectedOutput).to.eq(expectation.expectedOutput, `${messagePrefix}: expectedOutput`) + expect(failureDetails.location?.range.start.line).to.eq(expectation.line || 0, `${messagePrefix}: line number`) + expect(failureDetails.location?.uri.fsPath).to.eq(expectedTestItem.file, `${messagePrefix}: path`) } -export function setupMockTestController(): vscode.TestController { +export function setupMockTestController(rootLog?: IChildLogger): vscode.TestController { let mockTestController = mock() let createTestItem = (id: string, label: string, uri?: vscode.Uri | undefined) => { return { @@ -199,23 +213,32 @@ export function setupMockTestController(): vscode.TestController { busy: false, range: undefined, error: undefined, - children: new StubTestItemCollection(), + children: new StubTestItemCollection(rootLog || noop_logger(), instance(mockTestController)), } } when(mockTestController.createTestItem(anyString(), anyString())).thenCall(createTestItem) when(mockTestController.createTestItem(anyString(), anyString(), anything())).thenCall(createTestItem) - let testItems = new StubTestItemCollection() + let testItems = new StubTestItemCollection(rootLog || noop_logger(), instance(mockTestController)) when(mockTestController.items).thenReturn(testItems) return mockTestController } -export function setupMockRequest(testSuite: TestSuite, testId?: string): vscode.TestRunRequest { +export function setupMockRequest(testSuite: TestSuite, testId?: string | string[]): vscode.TestRunRequest { let mockRequest = mock() if (testId) { - let testItem = testSuite.getOrCreateTestItem(testId) - when(mockRequest.include).thenReturn([testItem]) + if (Array.isArray(testId)) { + let testItems: vscode.TestItem[] = [] + testId.forEach(id => { + let testItem = testSuite.getOrCreateTestItem(id) + testItems.push(testItem) + }) + when(mockRequest.include).thenReturn(testItems) + } else { + let testItem = testSuite.getOrCreateTestItem(testId as string) + when(mockRequest.include).thenReturn([testItem]) + } } else { - when(mockRequest.include).thenReturn([]) + when(mockRequest.include).thenReturn(undefined) } when(mockRequest.exclude).thenReturn([]) return mockRequest diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index eaa70d2..f9afee7 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -8,7 +8,7 @@ import { TestSuite } from '../../../src/testSuite'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { noop_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { @@ -62,7 +62,7 @@ suite('Extension Test for Minitest', function() { this.beforeEach(async function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) - testController = new StubTestController() + testController = new StubTestController(stdout_logger()) // Populate controller with test files. This would be done by the filesystem globs in the watchers let createTest = (id: string, label?: string) => @@ -74,9 +74,9 @@ suite('Extension Test for Minitest', function() { config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new MinitestTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + testSuite = new TestSuite(stdout_logger(), testController, config) + testRunner = new MinitestTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -183,7 +183,7 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! let args = testStateCaptors(mockTestRun) @@ -206,7 +206,7 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! let args = testStateCaptors(mockTestRun).failedArg(0) @@ -231,7 +231,7 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! let args = testStateCaptors(mockTestRun).erroredArg(0) @@ -256,7 +256,7 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource() await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! let args = testStateCaptors(mockTestRun) testItemMatches(args.startedArg(0), { diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 98117a1..8b0df72 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path' import { anything, instance, verify } from 'ts-mockito' +import { before, beforeEach } from 'mocha'; import { expect } from 'chai'; import { TestLoader } from '../../../src/testLoader'; @@ -8,7 +9,7 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; +import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -28,18 +29,18 @@ suite('Extension Test for RSpec', function() { file) } - this.beforeAll(function() { + before(function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) config = new RspecConfig(path.resolve("ruby"), workspaceFolder) }) suite('dry run', function() { - this.beforeEach(function () { - testController = new StubTestController() + beforeEach(function () { + testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -47,7 +48,7 @@ suite('Extension Test for RSpec', function() { let createTest = (id: string, label?: string) => testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) testController.items.add(createTest("abs_spec.rb")) - testController.items.add(createTest("square_spec.rb")) + testController.items.add(createTest("square/square_spec.rb")) let subfolderItem = createTest("subfolder") testController.items.add(subfolderItem) subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) @@ -62,9 +63,9 @@ suite('Extension Test for RSpec', function() { children: [] }, { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square/square_spec.rb", children: [] }, { @@ -115,9 +116,9 @@ suite('Extension Test for RSpec', function() { ] }, { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square/square_spec.rb", children: [] }, { @@ -171,416 +172,204 @@ suite('Extension Test for RSpec', function() { ] }, { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb", - label: "square_spec.rb", + file: expectedPath("square"), + id: "square", + label: "square", canResolveChildren: true, children: [ { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb[1:1]", - label: "finds the square of 2", - line: 3, - }, - { - file: expectedPath("square_spec.rb"), - id: "square_spec.rb[1:2]", - label: "finds the square of 3", - line: 7, - } - ] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", - canResolveChildren: true, - children: [ - { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square_spec.rb", canResolveChildren: true, children: [ { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb[1:1]", - label: "wibbles and wobbles", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", line: 3, + }, + { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + line: 7, } ] } ] - }, + } ] ) }) }) suite('status events', function() { - let cancellationTokenSource = new vscode.CancellationTokenSource() + let cancellationTokenSource = new vscode.CancellationTokenSource(); - suite('dry run before test run', function() { - this.beforeAll(async function () { - testController = new StubTestController() - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); - await testLoader.discoverAllFilesInWorkspace() - }) - - suite('file with passing and failing specs', function() { - let mockTestRun: vscode.TestRun - - this.beforeAll(async function() { - let mockRequest = setupMockRequest(testSuite, "square_spec.rb") - let request = instance(mockRequest) - await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - }) + before(async function() { + testController = new StubTestController(stdout_logger()) + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new RspecTestRunner(stdout_logger("debug"), workspaceFolder, testController, config, testSuite) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + await testLoader.discoverAllFilesInWorkspace() + }) - test('enqueued status event', async function() { - // Not really a useful status event unless you can queue up tests and run only - // parts of the queue at a time - perhaps in the future - verify(mockTestRun.enqueued(anything())).times(0) - }) + suite(`running collections emits correct statuses`, async function() { + let mockTestRun: vscode.TestRun + + test('when running full suite', async function() { + let mockRequest = setupMockRequest(testSuite) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.started(anything())).times(8) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) - test('started status event', function() { - let args = testStateCaptors(mockTestRun) + test('when running all top-level items', async function() { + let mockRequest = setupMockRequest(testSuite, ["abs_spec.rb", "square"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.started(anything())).times(8) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) - // Assert that all specs and the file are marked as started - expect(args.startedArg(0).id).to.eq("square_spec.rb") - expect(args.startedArg(1).id).to.eq("square_spec.rb[1:1]") - expect(args.startedArg(2).id).to.eq("square_spec.rb[1:2]") - verify(mockTestRun.started(anything())).times(3) - }) + test('when running all files', async function() { + let mockRequest = setupMockRequest(testSuite, ["abs_spec.rb", "square/square_spec.rb"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + verify(mockTestRun.enqueued(anything())).times(0) + // One less 'started' than the other tests as it doesn't include the 'square' folder + verify(mockTestRun.started(anything())).times(7) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + }) - test('passed status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:1]", - file: expectedPath("square_spec.rb"), + suite(`running single tests emits correct statuses`, async function() { + let params: {status: string, expectedTest: TestItemExpectation, failureExpectation?: TestFailureExpectation}[] = [ + { + status: "passed", + expectedTest: { + id: "square/square_spec.rb[1:1]", + file: expectedPath("square/square_spec.rb"), label: "finds the square of 2", line: 3 - } - - // Verify that passed status event occurred exactly twice (once as soon as it - // passed and again when parsing the test run output) - testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, - expectedTestItem) - testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, - expectedTestItem) - verify(mockTestRun.passed(anything(), anything())).times(2) - - // Expect failed status events for the other spec in the file - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - - test('failure status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:2]", - file: expectedPath("square_spec.rb"), + }, + }, + { + status: "failed", + expectedTest: { + id: "square/square_spec.rb[1:2]", + file: expectedPath("square/square_spec.rb"), label: "finds the square of 3", line: 7 - } - - // Verify that failed status event occurred twice - once immediately after the - // test failed with no details, and once at the end when parsing test output with - // more information - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, - }) - verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, + }, + failureExpectation: { message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", line: 8, - }) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - - // Expect passed status events for the other spec in the file - verify(mockTestRun.passed(anything(), anything())).times(2) - - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - }) - - suite('single specs from file with passing, skipped and errored specs', async function() { - let mockTestRun: vscode.TestRun - - test('single passing spec', async function() { - let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:1]") - let request = instance(mockRequest) - await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - - let expectedTestItem = { + } + }, + { + status: "passed", + expectedTest: { id: "abs_spec.rb[1:1]", file: expectedPath("abs_spec.rb"), label: "finds the absolute value of 1", line: 3 } - - // Verify that passed status event occurred exactly twice (once as soon as it - // passed and again when parsing the test run output) - testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, - expectedTestItem) - testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, - expectedTestItem) - verify(mockTestRun.passed(anything(), anything())).times(2) - expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTestItem.id) - verify(mockTestRun.started(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - - test('single spec with error', async function() { - let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:2]") - let request = instance(mockRequest) - await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - - let expectedTestItem = { + }, + { + status: "errored", + expectedTest: { id: "abs_spec.rb[1:2]", file: expectedPath("abs_spec.rb"), label: "finds the absolute value of 0", line: 7 - } - - // Verify that failed status event occurred twice - once immediately after the - // test failed with no details, and once at the end when parsing test output with - // more information - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, - }) - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { - testItem: expectedTestItem, + }, + failureExpectation: { message: "RuntimeError:\nAbs for zero is not supported", line: 8, - }) - verify(mockTestRun.failed(anything(), anything(), anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) - expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTestItem.id) - verify(mockTestRun.started(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - - test('single skipped spec', async function() { - let mockRequest = setupMockRequest(testSuite, "abs_spec.rb[1:3]") - let request = instance(mockRequest) - await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun) - let expectation = { + } + }, + { + status: "skipped", + expectedTest: { id: "abs_spec.rb[1:3]", file: expectedPath("abs_spec.rb"), label: "finds the absolute value of -1", line: 11 } - testItemMatches(args.skippedArg(0), expectation) - - // Verify that only expected status events occurred - verify(mockTestRun.enqueued(anything())).times(1) - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.skipped(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - }) - }) - }) - - suite('test run without dry run', function () { - this.beforeAll(function () { - testController = new StubTestController() - testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); - }) - - suite('file with passing and failing specs', function() { + } + ] + for(const {status, expectedTest, failureExpectation} of params) { let mockTestRun: vscode.TestRun - this.beforeAll(async function() { - let request = instance(setupMockRequest(testSuite, "square_spec.rb")) + beforeEach(async function() { + let mockRequest = setupMockRequest(testSuite, expectedTest.id) + let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun() - }) - - test('enqueued status event', async function() { - // Not really a useful status event unless you can queue up tests and run only - // parts of the queue at a time - perhaps in the future - verify(mockTestRun.enqueued(anything())).times(0) - }) - - test('started status event', function() { - let args = testStateCaptors(mockTestRun) - - // Assert that all specs and the file are marked as started - expect(args.startedArg(0).id).to.eq("square_spec.rb") - verify(mockTestRun.started(anything())).times(1) - }) - - test('passed status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:1]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 2", - line: 3 - } - - // Verify that passed status event occurred exactly twice (once as soon as it - // passed and again when parsing the test run output) - testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, - expectedTestItem) - testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, - expectedTestItem) - verify(mockTestRun.passed(anything(), anything())).times(2) - - // Expect failed status events for the other spec in the file - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! }) - test('failure status event', function() { - let expectedTestItem = { - id: "square_spec.rb[1:2]", - file: expectedPath("square_spec.rb"), - label: "finds the square of 3", - line: 7 + test(`id: ${expectedTest.id}, status: ${status}`, function() { + switch(status) { + case "passed": + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "failed": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "errored": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "skipped": + testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) + testItemMatches(testStateCaptors(mockTestRun).skippedArg(1), expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(2) + break; } - - // Verify that failed status event occurred twice - once immediately after the - // test failed with no details, and once at the end when parsing test output with - // more information - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, - }) - verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, { - testItem: expectedTestItem, - message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", - line: 8, - }) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - - // Expect passed status events for the other spec in the file - verify(mockTestRun.passed(anything(), anything())).times(2) - - // Verify that no other status events occurred - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - }) - - suite('single specs from file with passing, skipped and errored specs', async function() { - test('passing spec', async function() { - let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:1]")) - - await testRunner.runHandler(errorRequest, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let expectedTestItem = { - id: "abs_spec.rb[1:1]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 1", - line: 3, - } - // Verify that passed status event occurred exactly once (once as soon as it - // passed and again when parsing the test run output) - testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, - expectedTestItem) - - // Verify that only expected status events occurred + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTest.id) verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.passed(anything(), anything())).times(1) // Verify that no other status events occurred - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) }) - - test('errored spec', async function() { - let errorRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:2]")) - - await testRunner.runHandler(errorRequest, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let expectedTestItem = { - id: "abs_spec.rb[1:2]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 0", - line: 7, - } - // Verify that failed status occurred immediately and error status event occurred - // when parsing test output with more information - // verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, { - // testItem: expectedTestItem, - // }) - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, { - testItem: expectedTestItem, - message: "RuntimeError:\nAbs for zero is not supported", - line: 8, - }) - - // Verify that only expected status events occurred - verify(mockTestRun.started(anything())).times(1) - //verify(mockTestRun.failed(anything(), anything(), anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.skipped(anything())).times(0) - }) - - test('skipped spec', async function() { - let skippedRequest = instance(setupMockRequest(testSuite, "abs_spec.rb[1:3]")) - await testRunner.runHandler(skippedRequest, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun() - - let args = testStateCaptors(mockTestRun) - let expectation = { - id: "abs_spec.rb[1:3]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of -1", - line: 11 - } - testItemMatches(args.startedArg(0), expectation) - testItemMatches(args.skippedArg(0), expectation) - - // Verify that only expected status events occurred - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.skipped(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(0) - }) - }) + } }) }) }); diff --git a/test/suite/unitTests/testLoader.test.ts b/test/suite/unitTests/testLoader.test.ts deleted file mode 100644 index aec8f02..0000000 --- a/test/suite/unitTests/testLoader.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { expect } from "chai"; -import { before, beforeEach } from 'mocha'; -import { instance, mock, when } from 'ts-mockito' -import * as vscode from 'vscode' -import * as path from 'path' - -import { Config } from "../../../src/config"; -import { ParsedTest, TestLoader } from "../../../src/testLoader"; -import { TestSuite } from "../../../src/testSuite"; -import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; -import { noop_logger } from "../helpers"; -import { StubTestController } from '../../stubs/stubTestController'; - -suite('TestLoader', function () { - let testSuite: TestSuite - let testController: vscode.TestController - - const config = mock() - - suite('#parseDryRunOutput()', function () { - suite('RSpec output', function () { - before(function () { - when(config.getRelativeTestDirectory()).thenReturn('spec') - }) - - beforeEach(function () { - testController = new StubTestController() - testSuite = new TestSuite(noop_logger(), testController, instance(config)) - }) - - const examples: ParsedTest[] = [ - { - "id": "./spec/abs_spec.rb[1:1]", - "description": "finds the absolute value of 1", - "full_description": "Abs finds the absolute value of 1", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 4, - "type": null, - "pending_message": null - }, - { - "id": "./spec/abs_spec.rb[1:2]", - "description": "finds the absolute value of 0", - "full_description": "Abs finds the absolute value of 0", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 8, - "type": null, - "pending_message": null - }, - { - "id": "./spec/abs_spec.rb[1:3]", - "description": "finds the absolute value of -1", - "full_description": "Abs finds the absolute value of -1", - "status": "passed", - "file_path": "./spec/abs_spec.rb", - "line_number": 12, - "type": null, - "pending_message": null - } - ] - const parameters = examples.map( - (spec: ParsedTest, i: number, examples: ParsedTest[]) => { - return { - index: i, - spec: spec, - expected_location: i + 11, - expected_id: `abs_spec.rb[1:${i + 1}]`, - expected_file_path: 'abs_spec.rb' - } - } - ) - - parameters.forEach(({ - index, - spec, - expected_location, - expected_id, - expected_file_path - }) => { - test(`parses specs correctly - ${spec["id"]}`, function () { - let parsedSpec = TestLoader.parseDryRunOutput( - noop_logger(), - testSuite, - [spec] - )[0] - expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') - expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') - expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') - }) - }) - }) - - suite('Minitest output', function () { - before(function () { - when(config.getRelativeTestDirectory()).thenReturn('test') - }) - - beforeEach(function () { - testController = new StubTestController() - testSuite = new TestSuite(noop_logger(), testController, instance(config)) - }) - - const examples: ParsedTest[] = [ - { - "description": "abs positive", - "full_description": "abs positive", - "file_path": "./test/abs_test.rb", - "full_path": "home/foo/test/fixtures/minitest/test/abs_test.rb", - "line_number": 4, - "klass": "AbsTest", - "method": "test_abs_positive", - "runnable": "AbsTest", - "id": "./test/abs_test.rb[4]" - }, - { - "description": "abs 0", - "full_description": "abs 0", - "file_path": "./test/abs_test.rb", - "full_path": "/home/foo/test/fixtures/minitest/test/abs_test.rb", - "line_number": 8, - "klass": "AbsTest", - "method": "test_abs_0", - "runnable": "AbsTest", - "id": "./test/abs_test.rb[8]" - }, - { - "description": "abs negative", - "full_description": "abs negative", - "file_path": "./test/abs_test.rb", - "full_path": "/home/foo/test/fixtures/minitest/test/abs_test.rb", - "line_number": 12, - "klass": "AbsTest", - "method": "test_abs_negative", - "runnable": "AbsTest", - "id": "./test/abs_test.rb[12]" - }, - { - "description": "square 2", - "full_description": "square 2", - "file_path": "./test/square_test.rb", - "full_path": "/home/foo/test/fixtures/minitest/test/square_test.rb", - "line_number": 4, - "klass": "SquareTest", - "method": "test_square_2", - "runnable": "SquareTest", - "id": "./test/square_test.rb[4]" - }, - { - "description": "square 3", - "full_description": "square 3", - "file_path": "./test/square_test.rb", - "full_path": "/home/foo/test/fixtures/minitest/test/square_test.rb", - "line_number": 8, - "klass": "SquareTest", - "method": "test_square_3", - "runnable": "SquareTest", - "id": "./test/square_test.rb[8]" - } - ] - const parameters = examples.map( - (spec: ParsedTest, i: number, examples: ParsedTest[]) => { - return { - index: i, - spec: spec, - expected_location: examples[i]["line_number"], - expected_id: `${examples[i]["file_path"].replace('./test/', '')}[${examples[i]["line_number"]}]`, - expected_file_path: `${examples[i]["file_path"].replace('./test/', '')}` - } - } - ) - - parameters.forEach(({ - index, - spec, - expected_location, - expected_id, - expected_file_path - }) => { - test(`parses specs correctly - ${spec["id"]}`, function () { - let parsedSpec = TestLoader.parseDryRunOutput( - noop_logger(), - testSuite, - [spec] - )[0] - expect(parsedSpec['location']).to.eq(expected_location, 'location incorrect') - expect(parsedSpec['id']).to.eq(expected_id, 'id incorrect') - expect(parsedSpec['file_path']).to.eq(expected_file_path, 'file path incorrect') - }) - }) - }) - }) - - suite('getTestSuiteForFile', function() { - let mockTestRunner: RspecTestRunner - let testRunner: RspecTestRunner - let testLoader: TestLoader - let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] - let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') - let id = "abs_spec.rb" - let abs_spec_item: vscode.TestItem - let createTestItem = (id: string): vscode.TestItem => { - return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) - } - - this.beforeAll(function () { - when(config.getRelativeTestDirectory()).thenReturn('spec') - when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) - }) - - this.beforeEach(function () { - mockTestRunner = mock(RspecTestRunner) - testRunner = instance(mockTestRunner) - testController = new StubTestController() - testSuite = new TestSuite(noop_logger(), testController, instance(config)) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) - abs_spec_item = createTestItem(id) - testController.items.add(abs_spec_item) - }) - - test('creates test items from output', function () { - expect(abs_spec_item.children.size).to.eq(0) - - testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - expect(abs_spec_item.children.size).to.eq(1) - }) - - test('removes test items not in output', function () { - let missing_id = "abs_spec.rb[3:1]" - let missing_child_item = createTestItem(missing_id) - abs_spec_item.children.add(missing_child_item) - expect(abs_spec_item.children.size).to.eq(1) - - testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - expect(abs_spec_item.children.size).to.eq(1) - expect(abs_spec_item.children.get(missing_id)).to.be.undefined - expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined - }) - }) -}) diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts new file mode 100644 index 0000000..af79c0c --- /dev/null +++ b/test/suite/unitTests/testRunner.test.ts @@ -0,0 +1,250 @@ +//import { expect } from "chai"; +import { before, beforeEach } from 'mocha'; +import { instance, mock, when } from 'ts-mockito' +import * as vscode from 'vscode' +import * as path from 'path' + +import { Config } from "../../../src/config"; +import { TestSuite } from "../../../src/testSuite"; +import { TestRunner } from "../../../src/testRunner"; +import { RspecConfig } from '../../../src/rspec/rspecConfig'; +import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; +import { MinitestConfig } from '../../../src/minitest/minitestConfig'; +import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; +import { noop_logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; +import { StubTestController } from '../../stubs/stubTestController'; + +suite('TestRunner', function () { + let testSuite: TestSuite + let testController: vscode.TestController + let testRunner: TestRunner + + const config = mock() + + suite('#parseAndHandleTestOutput()', function () { + suite('RSpec output', function () { + before(function () { + let relativeTestPath = "spec" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + }) + + beforeEach(function () { + testController = new StubTestController(noop_logger()) + testSuite = new TestSuite(noop_logger(), testController, instance(config)) + testRunner = new RspecTestRunner(noop_logger(), undefined, testController, instance(config) as RspecConfig, testSuite) + }) + + const expectedTests: TestItemExpectation[] = [ + { + id: "square", + label: "square", + file: path.resolve("spec", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb", + label: "square_spec.rb", + file: path.resolve("spec", "square", "square_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 3, + }, + { + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 7, + }, + ] + } + ] + }, + { + id: "abs_spec.rb", + label: "abs_spec.rb", + file: path.resolve("spec", "abs_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + file: path.resolve("spec", "abs_spec.rb"), + line: 3, + }, + { + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + file: path.resolve("spec", "abs_spec.rb"), + line: 7, + }, + { + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + file: path.resolve("spec", "abs_spec.rb"), + line: 11, + } + ] + } + ] + const outputJson = { + "version":"3.10.1", + "examples":[ + {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, + {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} + ], + "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, + "summary_line":"6 examples, 0 failures" + } + const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + + test('parses specs correctly', function () { + testRunner.parseAndHandleTestOutput(output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + + suite('Minitest output', function () { + before(function () { + let relativeTestPath = "test" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + }) + + beforeEach(function () { + testController = new StubTestController(noop_logger()) + testSuite = new TestSuite(noop_logger(), testController, instance(config)) + testRunner = new MinitestTestRunner(noop_logger(), undefined, testController, instance(config) as MinitestConfig, testSuite) + }) + + const expectedTests: TestItemExpectation[] = [ + { + id: "square", + label: "square", + file: path.resolve("test", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb", + label: "square_test.rb", + file: path.resolve("test", "square", "square_test.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb[4]", + label: "square 2", + file: path.resolve("test", "square", "square_test.rb"), + line: 3, + }, + { + id: "square/square_test.rb[8]", + label: "square 3", + file: path.resolve("test", "square", "square_test.rb"), + line: 7, + }, + ] + } + ] + }, + { + id: "abs_test.rb", + label: "abs_test.rb", + file: path.resolve("test", "abs_test.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_test.rb[4]", + label: "abs positive", + file: path.resolve("test", "abs_test.rb"), + line: 3, + }, + { + id: "abs_test.rb[8]", + label: "abs 0", + file: path.resolve("test", "abs_test.rb"), + line: 7, + }, + { + id: "abs_test.rb[12]", + label: "abs negative", + file: path.resolve("test", "abs_test.rb"), + line: 11, + } + ] + }, + ] + const outputJson = { + "version":"5.14.4", + "examples":[ + {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, + {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, + {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, + {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, + {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} + ] + } + const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + + test('parses specs correctly', function () { + testRunner.parseAndHandleTestOutput(output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + }) + + // suite('getTestSuiteForFile', function() { + // let mockTestRunner: RspecTestRunner + // let testRunner: RspecTestRunner + // let testLoader: TestLoader + // let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] + // let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') + // let id = "abs_spec.rb" + // let abs_spec_item: vscode.TestItem + // let createTestItem = (id: string): vscode.TestItem => { + // return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) + // } + + // this.beforeAll(function () { + // when(config.getRelativeTestDirectory()).thenReturn('spec') + // when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) + // }) + + // this.beforeEach(function () { + // mockTestRunner = mock(RspecTestRunner) + // testRunner = instance(mockTestRunner) + // testController = new StubTestController() + // testSuite = new TestSuite(noop_logger(), testController, instance(config)) + // testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) + // abs_spec_item = createTestItem(id) + // testController.items.add(abs_spec_item) + // }) + + // test('creates test items from output', function () { + // expect(abs_spec_item.children.size).to.eq(0) + + // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + // expect(abs_spec_item.children.size).to.eq(1) + // }) + + // test('removes test items not in output', function () { + // let missing_id = "abs_spec.rb[3:1]" + // let missing_child_item = createTestItem(missing_id) + // abs_spec_item.children.add(missing_child_item) + // expect(abs_spec_item.children.size).to.eq(1) + + // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + // expect(abs_spec_item.children.size).to.eq(1) + // expect(abs_spec_item.children.get(missing_id)).to.be.undefined + // expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined + // }) + // }) +}) diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index 58bc3f7..fd9e61d 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -7,7 +7,6 @@ import path from 'path' import { Config } from '../../../src/config'; import { TestSuite } from '../../../src/testSuite'; import { StubTestController } from '../../stubs/stubTestController'; -import { StubTestItem } from '../../stubs/stubTestItem'; import { noop_logger, testUriMatches } from '../helpers'; suite('TestSuite', function () { @@ -23,7 +22,7 @@ suite('TestSuite', function () { }); beforeEach(function () { - controller = new StubTestController() + controller = new StubTestController(noop_logger()) testSuite = new TestSuite(noop_logger(), controller, instance(mockConfig)) }); @@ -53,11 +52,11 @@ suite('TestSuite', function () { const label = 'test-label' beforeEach(function () { - controller.items.add(new StubTestItem(id, label)) + controller.items.add(controller.createTestItem(id, label)) }) test('deletes only the specified test item', function () { - let secondTestItem = new StubTestItem('test-id-2', 'test-label-2') + let secondTestItem = controller.createTestItem('test-id-2', 'test-label-2') controller.items.add(secondTestItem) expect(controller.items.size).to.eq(2) @@ -79,10 +78,16 @@ suite('TestSuite', function () { suite('#getTestItem()', function () { const id = 'test-id' const label = 'test-label' - const testItem = new StubTestItem(id, label) const childId = 'folder/child-test' - const childItem = new StubTestItem(childId, 'child-test') - const folderItem = new StubTestItem('folder', 'folder') + let testItem: vscode.TestItem + let childItem: vscode.TestItem + let folderItem: vscode.TestItem + + before(function () { + testItem = controller.createTestItem(id, label) + childItem = controller.createTestItem(childId, 'child-test') + folderItem = controller.createTestItem('folder', 'folder') + }) beforeEach(function () { controller.items.add(testItem) @@ -114,9 +119,14 @@ suite('TestSuite', function () { suite('#getOrCreateTestItem()', function () { const id = 'test-id' const label = 'test-label' - const testItem = new StubTestItem(id, label) const childId = `folder${path.sep}child-test` - const childItem = new StubTestItem(childId, 'child-test') + let testItem: vscode.TestItem + let childItem: vscode.TestItem + + beforeEach(function () { + testItem = controller.createTestItem(id, label) + childItem = controller.createTestItem(childId, 'child-test') + }) test('gets the specified item if ID is found', function () { controller.items.add(testItem) @@ -131,7 +141,7 @@ suite('TestSuite', function () { }) test('gets the specified nested test if ID is found', function () { - let folderItem = new StubTestItem('folder', 'folder') + let folderItem = controller.createTestItem('folder', 'folder') controller.items.add(testItem) folderItem.children.add(childItem) controller.items.add(folderItem) @@ -141,7 +151,7 @@ suite('TestSuite', function () { test('creates item if nested ID is not found', function () { let id = `folder${path.sep}not-found` - let folderItem = new StubTestItem('folder', 'folder') + let folderItem = controller.createTestItem('folder', 'folder') controller.items.add(folderItem) let testItem = testSuite.getOrCreateTestItem(id) From f510125e5d1a3ddf7b8fee519f3c9a064039ff50 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 3 Jan 2023 21:49:16 +0000 Subject: [PATCH 051/108] WIP: redoing child process handling and testRunner structure --- src/config.ts | 23 ++ src/main.ts | 2 +- src/minitest/minitestTestRunner.ts | 3 +- src/rspec/rspecConfig.ts | 20 +- src/rspec/rspecTestRunner.ts | 10 +- src/testFactory.ts | 12 +- src/testRunner.ts | 173 +++++---- test/suite/minitest/minitest.test.ts | 449 ++++++++++++++---------- test/suite/rspec/rspec.test.ts | 171 ++++----- test/suite/unitTests/testRunner.test.ts | 4 +- 10 files changed, 453 insertions(+), 414 deletions(-) diff --git a/src/config.ts b/src/config.ts index 7773d6f..ec47abd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -57,6 +57,29 @@ export abstract class Config { return path.resolve(this.workspaceFolder?.uri.fsPath || '.', this.getRelativeTestDirectory()) } + /** + * Get the command to run a single spec/test + * + * @param testItem The testItem representing the test to be run + * @param debugConfiguration debug configuration + */ + public abstract getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string + + /** + * Get the command to run a spec/test file or folder + * + * @param testItem The testItem representing the file to be run + * @param debugConfiguration debug configuration + */ + public abstract getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string + + /** + * Get the command to run all tests in the suite + * + * @param debugConfiguration debug configuration + */ + public abstract getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string + /** * Get the env vars to run the subprocess with. * diff --git a/src/main.ts b/src/main.ts index c2f75d2..0f6c341 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext) { if (testFramework !== "none") { const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); - const testLoaderFactory = new TestFactory(log, workspace, controller, testConfig); + const testLoaderFactory = new TestFactory(log, controller, testConfig, workspace); context.subscriptions.push(controller); // TODO: REMOVE THIS! diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 627f014..801867b 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -6,7 +6,8 @@ import { TestRunContext } from '../testRunContext'; import { MinitestConfig } from './minitestConfig'; export class MinitestTestRunner extends TestRunner { - testFrameworkName = 'Minitest'; + // Minitest notifies on test start + canNotifyOnStartingTests: boolean = true /** * Perform a dry-run of the test suite to get information about every test. diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 6aac1fe..71ddfb8 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -60,18 +60,30 @@ export class RspecConfig extends Config { /** * Get test command with formatter and debugger arguments * - * @param debuggerConfig A VS Code debugger configuration. + * @param debugConfiguration A VS Code debugger configuration. * @return The test command */ - public testCommandWithFormatterAndDebugger(debuggerConfig?: vscode.DebugConfiguration): string { + public testCommandWithFormatterAndDebugger(debugConfiguration?: vscode.DebugConfiguration): string { let args = `--require ${this.getCustomFormatterLocation()} --format CustomFormatter` let cmd = `${this.getTestCommand()} ${args}` - if (debuggerConfig) { - cmd = this.getDebugCommand(debuggerConfig, args); + if (debugConfiguration) { + cmd = this.getDebugCommand(debugConfiguration, args); } return cmd } + public getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { + return `${this.testCommandWithFormatterAndDebugger(debugConfiguration)} '${this.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` + }; + + public getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { + return `${this.testCommandWithFormatterAndDebugger(debugConfiguration)} '${this.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` + }; + + public getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string { + return this.testCommandWithFormatterAndDebugger(debugConfiguration) + }; + /** * Get the env vars to run the subprocess with. * diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index e229eb7..abb536e 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -7,6 +7,9 @@ import { RspecConfig } from './rspecConfig'; import { ParsedTest } from 'src/testLoader'; export class RspecTestRunner extends TestRunner { + // RSpec only notifies on test completion + canNotifyOnStartingTests: boolean = false + /** * Perform a dry-run of the test suite to get information about every test. * @@ -15,9 +18,10 @@ export class RspecTestRunner extends TestRunner { async initTests(testItems: vscode.TestItem[]): Promise { let cmd = this.getListTestsCommand(testItems) - this.log.info(`Running dry-run of RSpec test suite with the following command: ${cmd}`); - this.log.debug(`cwd: ${__dirname}`) - this.log.debug(`child process cwd: ${this.workspace?.uri.fsPath}`) + this.log.info("Running dry-run of RSpec tests") + this.log.debug('command', cmd); + this.log.trace('cwd', __dirname) + this.log.trace('child process cwd', this.workspace?.uri.fsPath) // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { diff --git a/src/testFactory.ts b/src/testFactory.ts index 1249f28..f49adea 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -17,9 +17,9 @@ export class TestFactory implements vscode.Disposable { constructor( private readonly log: IVSCodeExtLogger, - private readonly workspace: vscode.WorkspaceFolder | undefined, private readonly controller: vscode.TestController, - private config: Config + private config: Config, + private readonly workspace?: vscode.WorkspaceFolder, ) { this.disposables.push(this.configWatcher()); this.framework = Config.getTestFramework(this.log); @@ -38,17 +38,17 @@ export class TestFactory implements vscode.Disposable { this.runner = this.framework == "rspec" ? new RspecTestRunner( this.log, - this.workspace, this.controller, this.config as RspecConfig, - this.testSuite + this.testSuite, + this.workspace, ) : new MinitestTestRunner( this.log, - this.workspace, this.controller, this.config as MinitestConfig, - this.testSuite + this.testSuite, + this.workspace, ) this.disposables.push(this.runner); } diff --git a/src/testRunner.ts b/src/testRunner.ts index 2ccd9fd..4bd6229 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -11,8 +11,8 @@ import { TestSuite } from './testSuite'; import { ParsedTest } from './testLoader'; export abstract class TestRunner implements vscode.Disposable { - protected currentChildProcess: childProcess.ChildProcess | undefined; - protected debugCommandStartedResolver: Function | undefined; + protected currentChildProcess?: childProcess.ChildProcess; + protected debugCommandStartedResolver?: Function; protected disposables: { dispose(): void }[] = []; protected readonly log: IChildLogger; @@ -25,10 +25,10 @@ export abstract class TestRunner implements vscode.Disposable { */ constructor( rootLog: IChildLogger, - protected workspace: vscode.WorkspaceFolder | undefined, protected controller: vscode.TestController, protected config: RspecConfig | MinitestConfig, protected testSuite: TestSuite, + protected workspace?: vscode.WorkspaceFolder, ) { this.log = rootLog.getChildLogger({label: "TestRunner"}) } @@ -39,6 +39,8 @@ export abstract class TestRunner implements vscode.Disposable { */ abstract initTests(testItems: vscode.TestItem[]): Promise + abstract canNotifyOnStartingTests: boolean + public dispose() { this.killChild(); for (const disposable of this.disposables) { @@ -115,61 +117,60 @@ export abstract class TestRunner implements vscode.Disposable { * @param process A process running the tests. * @return A promise that resolves when the test run completes. */ - async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { + async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; let log = this.log.getChildLogger({ label: `ChildProcess(${context.config.frameworkName()})` }) - let output: Promise = new Promise((resolve, reject) => { - let buffer: string | undefined - - process.on('exit', () => { - log.info('Child process has exited. Sending test run finish event.'); - this.currentChildProcess = undefined; - if(buffer) { - resolve(buffer); - } else { - let error = new Error("No output returned from child process") - log.error(error.message) - reject(error) - } - }); + let testIdsRun: vscode.TestItem[] = [] - process.stderr!.pipe(split2()).on('data', (data) => { - data = data.toString(); - log.trace(data); - if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { - this.debugCommandStartedResolver() - } - }); + process.stderr!.pipe(split2()).on('data', (data) => { + data = data.toString(); + log.trace(data); + if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { + this.debugCommandStartedResolver() + } + }) - process.stdout!.pipe(split2()).on('data', (data) => { - data = data.toString(); - log.trace(data); - let getTest = (testId: string): vscode.TestItem => { - testId = this.testSuite.normaliseTestId(testId) - return this.testSuite.getOrCreateTestItem(testId) - } - if (data.startsWith('PASSED:')) { - log.debug(`Received test status - PASSED`, data) - context.passed(getTest(data.replace('PASSED: ', ''))) - } else if (data.startsWith('FAILED:')) { - log.debug(`Received test status - FAILED`, data) - let testItem = getTest(data.replace('FAILED: ', '')) - let line = testItem.range?.start?.line ? testItem.range.start.line + 1 : 0 - context.failed(testItem, "", testItem.uri?.fsPath || "", line) - } else if (data.startsWith('RUNNING:')) { - log.debug(`Received test status - RUNNING`, data) - context.started(getTest(data.replace('RUNNING: ', ''))) - } else if (data.startsWith('PENDING:')) { - log.debug(`Received test status - PENDING`, data) - context.skipped(getTest(data.replace('PENDING: ', ''))) - } - if (data.includes('START_OF_TEST_JSON')) { - buffer = data; + process.stdout!.pipe(split2()).on('data', (data) => { + let getTest = (testId: string): vscode.TestItem => { + testId = this.testSuite.normaliseTestId(testId) + return this.testSuite.getOrCreateTestItem(testId) + } + if (data.startsWith('PASSED:')) { + log.debug(`Received test status - PASSED`, data) + context.passed(getTest(data.replace('PASSED: ', ''))) + } else if (data.startsWith('FAILED:')) { + log.debug(`Received test status - FAILED`, data) + let testItem = getTest(data.replace('FAILED: ', '')) + let line = testItem.range?.start?.line ? testItem.range.start.line + 1 : 0 + context.failed(testItem, "", testItem.uri?.fsPath || "", line) + } else if (data.startsWith('RUNNING:')) { + log.debug(`Received test status - RUNNING`, data) + context.started(getTest(data.replace('RUNNING: ', ''))) + } else if (data.startsWith('PENDING:')) { + log.debug(`Received test status - PENDING`, data) + context.skipped(getTest(data.replace('PENDING: ', ''))) + } else if (data.includes('START_OF_TEST_JSON')) { + log.trace("Received test run results", data); + testIdsRun = testIdsRun.concat(this.parseAndHandleTestOutput(data, context)); + } else { + log.trace("Ignoring unrecognised output", data) + } + }); + + await new Promise((resolve, reject) => { + process.once('exit', (code: number, signal: string) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Exit with error, code: ${code}, signal: ${signal}`)); } }); + process.once('error', (err: Error) => { + reject(err); + }); }) - return await output + return testIdsRun }; /** @@ -244,14 +245,14 @@ export abstract class TestRunner implements vscode.Disposable { continue; } - await this.runNode(test, context); + await this.runNode(context, test); } if (token.isCancellationRequested) { log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } else { log.trace('Running all tests in suite'); - await this.runNode(null, context); + await this.runNode(context); } } finally { @@ -304,51 +305,49 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context */ protected async runNode( - node: vscode.TestItem | null, - context: TestRunContext + context: TestRunContext, + node?: vscode.TestItem, ): Promise { let log = this.log.getChildLogger({label: this.runNode.name}) // Special case handling for the root suite, since it can be run // with runFullTestSuite() try { + let testsRun: vscode.TestItem[] if (node == null) { log.debug("Running all tests") this.controller.items.forEach((testSuite) => { // Mark selected tests as started - this.markTestAndChildrenStarted(testSuite, context) + this.enqueTestAndChildren(testSuite, context) }) - let testOutput = await this.runFullTestSuite(context); - this.parseAndHandleTestOutput(testOutput, context, undefined) - // If the suite is a file, run the tests as a file rather than as separate tests. + testsRun = await this.runFullTestSuite(context) } else if (node.canResolveChildren) { + // If the suite is a file, run the tests as a file rather than as separate tests. log.debug(`Running test file/folder: ${node.id}`) // Mark selected tests as started - this.markTestAndChildrenStarted(node, context) - - let testOutput = await this.runTestFile(node, context); + this.enqueTestAndChildren(node, context) - this.parseAndHandleTestOutput(testOutput, context, node) + testsRun = await this.runTestFile(context, node) } else { if (node.uri !== undefined) { log.debug(`Running single test: ${node.id}`) - this.markTestAndChildrenStarted(node, context) + this.enqueTestAndChildren(node, context) // Run the test at the given line, add one since the line is 0-indexed in // VS Code and 1-indexed for RSpec/Minitest. - let testOutput = await this.runSingleTest(node, context); - - this.parseAndHandleTestOutput(testOutput, context, node) + testsRun = await this.runSingleTest(context, node); } else { log.error("test missing file path") + return } } + this.testSuite.removeMissingTests(testsRun, node) } finally { context.testRun.end() } } - public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext, testItem?: vscode.TestItem) { + public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext): vscode.TestItem[] { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); log.trace('Parsing the below JSON:'); @@ -395,22 +394,15 @@ export abstract class TestRunner implements vscode.Disposable { newTestItem.label = description newTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); parsedTests.push(newTestItem) - let parent = newTestItem.parent - while (parent) { - if (!parsedTests.includes(parent)) { - parsedTests.push(parent) - } - parent = parent.parent - } log.trace("Parsed test", test) if(context) { // Only handle status if actual test run, not dry run this.handleStatus(test, context); } }); + return parsedTests } - this.testSuite.removeMissingTests(parsedTests, testItem) - + return [] } /** @@ -437,19 +429,14 @@ export abstract class TestRunner implements vscode.Disposable { * Mark a test node and all its children as being queued for execution */ private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { - context.enqueued(test); - if (test.children && test.children.size > 0) { - test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) + if (this.canNotifyOnStartingTests) { + // Tests will be marked as started as the runner gets to them + context.enqueued(test); + } else { + context.started(test); } - } - - /** - * Mark a test node and all its children as being queued for execution - */ - private markTestAndChildrenStarted(test: vscode.TestItem, context: TestRunContext) { - context.started(test); if (test.children && test.children.size > 0) { - test.children.forEach(child => { this.markTestAndChildrenStarted(child, context) }) + test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) } } @@ -461,7 +448,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test suite. */ - async runTestFramework(testCommand: string, type: string, context: TestRunContext): Promise { + async runTestFramework(testCommand: string, type: string, context: TestRunContext): Promise { this.log.trace(`Running test suite: ${type}`); return await this.spawnCancellableChild(testCommand, context) @@ -475,7 +462,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { + protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { context.token.onCancellationRequested(() => { this.log.debug("Cancellation requested") this.killChild() @@ -504,7 +491,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test. */ - protected async runSingleTest(testItem: vscode.TestItem, context: TestRunContext): Promise { + protected async runSingleTest(context: TestRunContext, testItem: vscode.TestItem): Promise { this.log.info(`Running single test: ${testItem.id}`); return await this.runTestFramework( this.getSingleTestCommand(testItem, context), @@ -519,8 +506,8 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the tests. */ - protected async runTestFile(testItem: vscode.TestItem, context: TestRunContext): Promise { - this.log.info(`Running test file: ${testItem}`); + protected async runTestFile(context: TestRunContext, testItem: vscode.TestItem): Promise { + this.log.info(`Running test file: ${testItem.id}`); return await this.runTestFramework( this.getTestFileCommand(testItem, context), "test file", @@ -533,7 +520,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context * @return The raw output from running the test suite. */ - protected async runFullTestSuite(context: TestRunContext): Promise { + protected async runFullTestSuite(context: TestRunContext): Promise { this.log.info(`Running full test suite.`); return await this.runTestFramework( this.getFullTestSuiteCommand(context), diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index f9afee7..b749027 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -2,13 +2,14 @@ import * as vscode from 'vscode'; import * as path from 'path' import { anything, instance, verify } from 'ts-mockito' import { expect } from 'chai'; +import { before, beforeEach } from 'mocha'; import { TestLoader } from '../../../src/testLoader'; import { TestSuite } from '../../../src/testSuite'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { setupMockRequest, stdout_logger, testItemCollectionMatches, testItemMatches, testStateCaptors } from '../helpers'; +import { noop_logger, setupMockRequest, stdout_logger, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { @@ -59,218 +60,276 @@ suite('Extension Test for Minitest', function() { line: 7 } - this.beforeEach(async function () { + before(function () { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) - testController = new StubTestController(stdout_logger()) + config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) + }) - // Populate controller with test files. This would be done by the filesystem globs in the watchers - let createTest = (id: string, label?: string) => - testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) - testController.items.add(createTest("abs_test.rb")) - let squareFolder = createTest("square") - testController.items.add(squareFolder) - squareFolder.children.add(createTest("square/square_test.rb", "square_test.rb")) + suite('dry run', function() { + beforeEach(function () { + testController = new StubTestController(stdout_logger()) + testSuite = new TestSuite(stdout_logger("debug"), testController, config) + testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + }) - config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) + test('Load tests on file resolve request', async function () { + // Populate controller with test files. This would be done by the filesystem globs in the watchers + let createTest = (id: string, label?: string) => { + let item = testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + item.canResolveChildren = true + return item + } + testController.items.add(createTest("abs_test.rb")) + let subfolderItem = createTest("square") + testController.items.add(subfolderItem) + subfolderItem.children.add(createTest("square/square_test.rb", "square_test.rb")) + + // No tests in suite initially, only test files and folders + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + canResolveChildren: true, + children: [] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + canResolveChildren: true, + children: [] + }, + ] + }, + ] + ) + + // Resolve a file (e.g. by clicking on it in the test explorer) + await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + + // Tests in that file have now been added to suite + testItemCollectionMatches(testController.items, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + canResolveChildren: true, + children: [] + }, + ], + }, + ] + ) + }) - testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new MinitestTestRunner(stdout_logger(), workspaceFolder, testController, config, testSuite) - testLoader = new TestLoader(stdout_logger(), testController, testRunner, config, testSuite); + test('Load all tests', async function () { + await testLoader.discoverAllFilesInWorkspace() + + const testSuite = testController.items + + testItemCollectionMatches(testSuite, + [ + { + file: expectedPath("abs_test.rb"), + id: "abs_test.rb", + label: "abs_test.rb", + canResolveChildren: true, + children: [ + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation + ] + }, + { + file: expectedPath("square"), + id: "square", + label: "square", + canResolveChildren: true, + children: [ + { + file: expectedPath("square/square_test.rb"), + id: "square/square_test.rb", + label: "square_test.rb", + canResolveChildren: true, + children: [ + square_2_expectation, + square_3_expectation + ] + }, + ], + }, + ] + ) + }) }) - test('Load tests on file resolve request', async function () { - // No tests in suite initially, only test files and folders - testItemCollectionMatches(testController.items, - [ - { - file: expectedPath("abs_test.rb"), - id: "abs_test.rb", - label: "abs_test.rb", - children: [] - }, - { - file: expectedPath("square"), - id: "square", - label: "square", - children: [ - { - file: expectedPath("square/square_test.rb"), - id: "square/square_test.rb", - label: "square_test.rb", - children: [] - }, - ] - }, - ] - ) + suite('status events', function() { + let cancellationTokenSource = new vscode.CancellationTokenSource(); + + before(async function() { + testController = new StubTestController(stdout_logger()) + testSuite = new TestSuite(noop_logger(), testController, config) + testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) + testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + await testLoader.discoverAllFilesInWorkspace() + }) - // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + suite(`running collections emits correct statuses`, async function() { + let mockTestRun: vscode.TestRun + + test('when running full suite', async function() { + let mockRequest = setupMockRequest(testSuite) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all top-level items', async function() { + let mockRequest = setupMockRequest(testSuite, ["abs_test.rb", "square"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + + test('when running all files', async function() { + let mockRequest = setupMockRequest(testSuite, ["abs_test.rb", "square/square_test.rb"]) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + + // One less 'started' than the other tests as it doesn't include the 'square' folder + verify(mockTestRun.enqueued(anything())).times(7) + verify(mockTestRun.started(anything())).times(5) + verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.failed(anything(), anything(), anything())).times(3) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(2) + }) + }) - // Tests in that file have now been added to suite - testItemCollectionMatches(testController.items, - [ + suite(`running single tests emits correct statuses`, async function() { + let params: {status: string, expectedTest: TestItemExpectation, failureExpectation?: TestFailureExpectation}[] = [ { - file: expectedPath("abs_test.rb"), - id: "abs_test.rb", - label: "abs_test.rb", - children: [ - abs_positive_expectation, - abs_zero_expectation, - abs_negative_expectation - ] + status: "passed", + expectedTest: square_2_expectation, }, { - file: expectedPath("square"), - id: "square", - label: "square", - children: [ - { - file: expectedPath("square/square_test.rb"), - id: "square/square_test.rb", - label: "square_test.rb", - children: [] - }, - ], + status: "failed", + expectedTest: square_3_expectation, + failureExpectation: { + message: "Expected: 9\n Actual: 6\n", + line: 8, + } }, - ] - ) - }) - - test('Load all tests', async () => { - await testLoader.discoverAllFilesInWorkspace() - - const testSuite = testController.items - - testItemCollectionMatches(testSuite, - [ { - file: expectedPath("abs_test.rb"), - id: "abs_test.rb", - label: "abs_test.rb", - children: [ - abs_positive_expectation, - abs_zero_expectation, - abs_negative_expectation - ] + status: "passed", + expectedTest: abs_positive_expectation }, { - file: expectedPath("square"), - id: "square", - label: "square", - children: [ - { - file: expectedPath("square/square_test.rb"), - id: "square/square_test.rb", - label: "square_test.rb", - children: [ - square_2_expectation, - square_3_expectation - ] - }, - ], + status: "errored", + expectedTest: abs_zero_expectation, + failureExpectation: { + message: "RuntimeError:\nAbs for zero is not supported", + line: 8, + } }, + { + status: "skipped", + expectedTest: abs_negative_expectation + } ] - ) - }) - - test('run test success', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) - - let mockRequest = setupMockRequest(testSuite, "square/square_test.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! - - let args = testStateCaptors(mockTestRun) - - // Passed called twice per test in file during dry run - testItemMatches(args.passedArg(0)["testItem"], square_2_expectation) - testItemMatches(args.passedArg(1)["testItem"], square_2_expectation) - testItemMatches(args.passedArg(2)["testItem"], square_3_expectation) - testItemMatches(args.passedArg(3)["testItem"], square_3_expectation) - - // Passed called again for passing test but not for failing test - testItemMatches(args.passedArg(4)["testItem"], square_2_expectation) - verify(mockTestRun.passed(anything(), undefined)).times(5) - }) - - test('run test failure', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("square/square_test.rb"))) - - let mockRequest = setupMockRequest(testSuite, "square/square_test.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! - - let args = testStateCaptors(mockTestRun).failedArg(0) - - testItemMatches(args.testItem, square_3_expectation) - - expect(args.message.message).to.contain("Expected: 9\n Actual: 6\n") - expect(args.message.actualOutput).to.be.undefined - expect(args.message.expectedOutput).to.be.undefined - expect(args.message.location?.range.start.line).to.eq(8) - expect(args.message.location?.uri.fsPath).to.eq(square_3_expectation.file) - expect(args.message.location?.uri.fsPath).to.eq(expectedPath("square/square_test.rb")) - - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(1) - }) - - test('run test error', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) - - let mockRequest = setupMockRequest(testSuite, "abs_test.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! - - let args = testStateCaptors(mockTestRun).erroredArg(0) - - testItemMatches(args.testItem, abs_zero_expectation) - - expect(args.message.message).to.match(/RuntimeError: Abs for zero is not supported/) - expect(args.message.actualOutput).to.be.undefined - expect(args.message.expectedOutput).to.be.undefined - expect(args.message.location?.range.start.line).to.eq(8) - expect(args.message.location?.uri.fsPath).to.eq(abs_zero_expectation.file) - expect(args.message.location?.uri.fsPath).to.eq(expectedPath("abs_test.rb")) - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.failed(anything(), anything(), undefined)).times(0) - verify(mockTestRun.errored(anything(), anything(), undefined)).times(1) - }) - - test('run test skip', async function() { - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) - - let mockRequest = setupMockRequest(testSuite, "abs_test.rb") - let request = instance(mockRequest) - let cancellationTokenSource = new vscode.CancellationTokenSource() - await testRunner.runHandler(request, cancellationTokenSource.token) - - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! - - let args = testStateCaptors(mockTestRun) - testItemMatches(args.startedArg(0), { - file: expectedPath("abs_test.rb"), - id: "abs_test.rb", - label: "abs_test.rb", - children: [ - abs_positive_expectation, - abs_zero_expectation, - abs_negative_expectation - ] + for(const {status, expectedTest, failureExpectation} of params) { + let mockTestRun: vscode.TestRun + + beforeEach(async function() { + let mockRequest = setupMockRequest(testSuite, expectedTest.id) + let request = instance(mockRequest) + await testRunner.runHandler(request, cancellationTokenSource.token) + mockTestRun = (testController as StubTestController).getMockTestRun(request)! + }) + + test(`id: ${expectedTest.id}, status: ${status}`, function() { + switch(status) { + case "passed": + testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) + testItemMatches(testStateCaptors(mockTestRun).passedArg(1).testItem, expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "failed": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "errored": + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.skipped(anything())).times(0) + break; + case "skipped": + testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) + testItemMatches(testStateCaptors(mockTestRun).skippedArg(1), expectedTest) + verify(mockTestRun.passed(anything(), anything())).times(0) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(0) + verify(mockTestRun.skipped(anything())).times(2) + break; + } + expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTest.id) + verify(mockTestRun.started(anything())).times(1) + verify(mockTestRun.enqueued(anything())).times(1) + }) + } }) - testItemMatches(args.skippedArg(0), abs_negative_expectation) - verify(mockTestRun.started(anything())).times(1) - verify(mockTestRun.skipped(anything())).times(1) }) }); diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 8b0df72..856ddc1 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -29,6 +29,37 @@ suite('Extension Test for RSpec', function() { file) } + let abs_positive_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + line: 3, + } + let abs_zero_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + line: 7, + } + let abs_negative_expectation = { + file: expectedPath("abs_spec.rb"), + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + line: 11, + } + let square_2_expectation = { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", + line: 3, + } + let square_3_expectation = { + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + line: 7, + } + before(function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) @@ -39,7 +70,7 @@ suite('Extension Test for RSpec', function() { beforeEach(function () { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(noop_logger(), workspaceFolder, testController, config, testSuite) + testRunner = new RspecTestRunner(noop_logger(), testController, config, testSuite, workspaceFolder) testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); }) @@ -48,10 +79,9 @@ suite('Extension Test for RSpec', function() { let createTest = (id: string, label?: string) => testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) testController.items.add(createTest("abs_spec.rb")) - testController.items.add(createTest("square/square_spec.rb")) - let subfolderItem = createTest("subfolder") + let subfolderItem = createTest("square") testController.items.add(subfolderItem) - subfolderItem.children.add(createTest("subfolder/foo_spec.rb", "foo_spec.rb")) + subfolderItem.children.add(createTest("square/square_spec.rb", "square_spec.rb")) // No tests in suite initially, only test files and folders testItemCollectionMatches(testController.items, @@ -63,22 +93,16 @@ suite('Extension Test for RSpec', function() { children: [] }, { - file: expectedPath("square/square_spec.rb"), - id: "square/square_spec.rb", - label: "square/square_spec.rb", - children: [] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", + file: expectedPath("square"), + id: "square", + label: "square", children: [ { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square_spec.rb", children: [] - } + }, ] }, ] @@ -95,43 +119,22 @@ suite('Extension Test for RSpec', function() { id: "abs_spec.rb", label: "abs_spec.rb", children: [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - } + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation ] }, { - file: expectedPath("square/square_spec.rb"), - id: "square/square_spec.rb", - label: "square/square_spec.rb", - children: [] - }, - { - file: expectedPath("subfolder"), - id: "subfolder", - label: "subfolder", + file: expectedPath("square"), + id: "square", + label: "square", children: [ { - file: expectedPath(path.join("subfolder", "foo_spec.rb")), - id: "subfolder/foo_spec.rb", - label: "foo_spec.rb", + file: expectedPath("square/square_spec.rb"), + id: "square/square_spec.rb", + label: "square_spec.rb", children: [] - } + }, ] }, ] @@ -151,24 +154,9 @@ suite('Extension Test for RSpec', function() { label: "abs_spec.rb", canResolveChildren: true, children: [ - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - line: 3, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - line: 7, - }, - { - file: expectedPath("abs_spec.rb"), - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - line: 11, - } + abs_positive_expectation, + abs_zero_expectation, + abs_negative_expectation ] }, { @@ -183,18 +171,8 @@ suite('Extension Test for RSpec', function() { label: "square_spec.rb", canResolveChildren: true, children: [ - { - file: expectedPath("square/square_spec.rb"), - id: "square/square_spec.rb[1:1]", - label: "finds the square of 2", - line: 3, - }, - { - file: expectedPath("square/square_spec.rb"), - id: "square/square_spec.rb[1:2]", - label: "finds the square of 3", - line: 7, - } + square_2_expectation, + square_3_expectation ] } ] @@ -210,7 +188,7 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger("debug"), workspaceFolder, testController, config, testSuite) + testRunner = new RspecTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) @@ -266,21 +244,11 @@ suite('Extension Test for RSpec', function() { let params: {status: string, expectedTest: TestItemExpectation, failureExpectation?: TestFailureExpectation}[] = [ { status: "passed", - expectedTest: { - id: "square/square_spec.rb[1:1]", - file: expectedPath("square/square_spec.rb"), - label: "finds the square of 2", - line: 3 - }, + expectedTest: square_2_expectation, }, { status: "failed", - expectedTest: { - id: "square/square_spec.rb[1:2]", - file: expectedPath("square/square_spec.rb"), - label: "finds the square of 3", - line: 7 - }, + expectedTest: square_3_expectation, failureExpectation: { message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", line: 8, @@ -288,21 +256,11 @@ suite('Extension Test for RSpec', function() { }, { status: "passed", - expectedTest: { - id: "abs_spec.rb[1:1]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 1", - line: 3 - } + expectedTest: abs_positive_expectation }, { status: "errored", - expectedTest: { - id: "abs_spec.rb[1:2]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of 0", - line: 7 - }, + expectedTest: abs_zero_expectation, failureExpectation: { message: "RuntimeError:\nAbs for zero is not supported", line: 8, @@ -310,12 +268,7 @@ suite('Extension Test for RSpec', function() { }, { status: "skipped", - expectedTest: { - id: "abs_spec.rb[1:3]", - file: expectedPath("abs_spec.rb"), - label: "finds the absolute value of -1", - line: 11 - } + expectedTest: abs_negative_expectation } ] for(const {status, expectedTest, failureExpectation} of params) { diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts index af79c0c..a95d08f 100644 --- a/test/suite/unitTests/testRunner.test.ts +++ b/test/suite/unitTests/testRunner.test.ts @@ -32,7 +32,7 @@ suite('TestRunner', function () { beforeEach(function () { testController = new StubTestController(noop_logger()) testSuite = new TestSuite(noop_logger(), testController, instance(config)) - testRunner = new RspecTestRunner(noop_logger(), undefined, testController, instance(config) as RspecConfig, testSuite) + testRunner = new RspecTestRunner(noop_logger(), testController, instance(config) as RspecConfig, testSuite) }) const expectedTests: TestItemExpectation[] = [ @@ -121,7 +121,7 @@ suite('TestRunner', function () { beforeEach(function () { testController = new StubTestController(noop_logger()) testSuite = new TestSuite(noop_logger(), testController, instance(config)) - testRunner = new MinitestTestRunner(noop_logger(), undefined, testController, instance(config) as MinitestConfig, testSuite) + testRunner = new MinitestTestRunner(noop_logger(), testController, instance(config) as MinitestConfig, testSuite) }) const expectedTests: TestItemExpectation[] = [ From 97074d4887ce72f70c985d966a526723edcae656 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 3 Jan 2023 22:58:37 +0000 Subject: [PATCH 052/108] Finally get rid of initTests and start using a TestRunProfile instead --- src/config.ts | 13 ++- src/main.ts | 48 ++++++---- src/minitest/minitestConfig.ts | 19 ++++ src/minitest/minitestTestRunner.ts | 56 ------------ src/rspec/rspecConfig.ts | 10 ++ src/rspec/rspecTestRunner.ts | 84 ----------------- src/testFactory.ts | 3 +- src/testLoader.ts | 11 +-- src/testRunContext.ts | 2 - src/testRunner.ts | 132 ++++----------------------- test/suite/minitest/minitest.test.ts | 11 ++- test/suite/rspec/rspec.test.ts | 19 +++- 12 files changed, 112 insertions(+), 296 deletions(-) diff --git a/src/config.ts b/src/config.ts index ec47abd..80a6fcc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,7 +58,7 @@ export abstract class Config { } /** - * Get the command to run a single spec/test + * Gets the command to run a single spec/test * * @param testItem The testItem representing the test to be run * @param debugConfiguration debug configuration @@ -66,7 +66,7 @@ export abstract class Config { public abstract getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string /** - * Get the command to run a spec/test file or folder + * Gets the command to run tests in a given file. * * @param testItem The testItem representing the file to be run * @param debugConfiguration debug configuration @@ -74,12 +74,19 @@ export abstract class Config { public abstract getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string /** - * Get the command to run all tests in the suite + * Gets the command to run the full test suite for the current workspace. * * @param debugConfiguration debug configuration */ public abstract getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string + /** + * Gets the command to resolve some or all of the tests in the suite + * + * @param testItems Array of TestItems to resolve children of, or undefined to resolve all tests + */ + public abstract getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string + /** * Get the env vars to run the subprocess with. * diff --git a/src/main.ts b/src/main.ts index 0f6c341..7930940 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,13 +67,36 @@ export async function activate(context: vscode.ExtensionContext) { if (testFramework !== "none") { const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); - const testLoaderFactory = new TestFactory(log, controller, testConfig, workspace); - context.subscriptions.push(controller); - // TODO: REMOVE THIS! - // Temporary kludge to clear out stale items that have bad data during development - // Later on will add context menus to delete test items or force a refresh of everything - controller.items.replace([]) + // TODO: (?) Add a "Profile" profile for profiling tests + let profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { + // Default run profile for running tests + runProfile: controller.createRunProfile( + 'Run', + vscode.TestRunProfileKind.Run, + (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + true // Default run profile + ), + + // Run profile for dry runs/getting test details + resolveTestsProfile: controller.createRunProfile( + 'ResolveTests', + vscode.TestRunProfileKind.Run, + (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + false + ), + + // Run profile for debugging tests + debugProfile: controller.createRunProfile( + 'Debug', + vscode.TestRunProfileKind.Debug, + (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig), + true + ), + } + + const testLoaderFactory = new TestFactory(log, controller, testConfig, profiles, workspace); + context.subscriptions.push(controller); testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); @@ -86,19 +109,6 @@ export async function activate(context: vscode.ExtensionContext) { await testLoaderFactory.getLoader().parseTestsInFile(test); } }; - - // TODO: (?) Add a "Profile" profile for profiling tests - controller.createRunProfile( - 'Run', - vscode.TestRunProfileKind.Run, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token), - true // Default run profile - ); - controller.createRunProfile( - 'Debug', - vscode.TestRunProfileKind.Debug, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig) - ); } else { log.fatal('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index 9c111a9..b44523b 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -57,6 +57,25 @@ export class MinitestConfig extends Config { || path.join('.', 'test'); } + public getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { + let line = testItem.id.split(':').pop(); + let relativeLocation = path.join(this.getAbsoluteTestDirectory(), testItem.id) + return `${this.testCommandWithDebugger(debugConfiguration)} '${relativeLocation}:${line}'` + }; + + public getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { + let relativeFile = path.join(this.getAbsoluteTestDirectory(), testItem.id) + return `${this.testCommandWithDebugger(debugConfiguration)} '${relativeFile}'` + }; + + public getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string { + return this.testCommandWithDebugger(debugConfiguration) + }; + + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + return `${this.getTestCommand()} vscode:minitest:list` + } + /** * Get the env vars to run the subprocess with. * diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 801867b..2c541ec 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -1,66 +1,10 @@ -import * as vscode from 'vscode'; -import * as path from 'path' -import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; -import { MinitestConfig } from './minitestConfig'; export class MinitestTestRunner extends TestRunner { // Minitest notifies on test start canNotifyOnStartingTests: boolean = true - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the Minitest JSON formatter. - */ - async initTests(testItems: vscode.TestItem[]): Promise { - let cmd = this.getListTestsCommand(testItems) - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace?.uri.fsPath, - maxBuffer: 8192 * 8192, - env: this.config.getProcessEnv() - }; - - this.log.info(`Getting a list of Minitest tests in suite with the following command: ${cmd}`); - - let output: Promise = new Promise((resolve, reject) => { - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - this.log.error(`Error while finding Minitest test suite: ${err.message}`); - this.log.error(`Output: ${stdout}`); - // Show an error message. - vscode.window.showWarningMessage("Ruby Test Explorer failed to find a Minitest test suite. Make sure Minitest is installed and your configured Minitest command is correct."); - vscode.window.showErrorMessage(err.message); - reject(err); - } - resolve(stdout); - }); - }); - return await output - }; - - protected getListTestsCommand(testItems?: vscode.TestItem[]): string { - return `${(this.config as MinitestConfig).getTestCommand()} vscode:minitest:list` - } - - protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { - let line = testItem.id.split(':').pop(); - let relativeLocation = `${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}` - return `${(this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig)} '${relativeLocation}:${line}'` - }; - - protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { - let relativeFile = `${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}` - return `${(this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig)} '${relativeFile}'` - }; - - protected getFullTestSuiteCommand(context: TestRunContext): string { - return (this.config as MinitestConfig).testCommandWithDebugger(context.debuggerConfig) - }; - /** * Handles test state based on the output returned by the Minitest Rake task. * diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 71ddfb8..b581445 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -84,6 +84,16 @@ export class RspecConfig extends Config { return this.testCommandWithFormatterAndDebugger(debugConfiguration) }; + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + let cmd = `${this.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; + + testItems?.forEach((item) => { + let testPath = path.join(this.getAbsoluteTestDirectory(), item.id) + cmd = `${cmd} "${testPath}"` + }) + return cmd + } + /** * Get the env vars to run the subprocess with. * diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index abb536e..7cf8303 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,72 +1,11 @@ -import * as vscode from 'vscode'; -import * as path from 'path' -import * as childProcess from 'child_process'; import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; -import { RspecConfig } from './rspecConfig'; import { ParsedTest } from 'src/testLoader'; export class RspecTestRunner extends TestRunner { // RSpec only notifies on test completion canNotifyOnStartingTests: boolean = false - /** - * Perform a dry-run of the test suite to get information about every test. - * - * @return The raw output from the RSpec JSON formatter. - */ - async initTests(testItems: vscode.TestItem[]): Promise { - let cmd = this.getListTestsCommand(testItems) - - this.log.info("Running dry-run of RSpec tests") - this.log.debug('command', cmd); - this.log.trace('cwd', __dirname) - this.log.trace('child process cwd', this.workspace?.uri.fsPath) - - // Allow a buffer of 64MB. - const execArgs: childProcess.ExecOptions = { - cwd: this.workspace?.uri.fsPath, - maxBuffer: 8192 * 8192, - }; - - let output: Promise = new Promise((resolve, reject) => { - childProcess.exec(cmd, execArgs, (err, stdout) => { - if (err) { - if (err.message.includes('deprecated')) { - this.log.warn(`Warning while finding RSpec test suite: ${err.message}`) - } else { - this.log.error(`Error while finding RSpec test suite: ${err.message}`); - // Show an error message. - vscode.window.showWarningMessage( - "Ruby Test Explorer failed to find an RSpec test suite. Make sure RSpec is installed and your configured RSpec command is correct.", - "View error message" - ).then(selection => { - if (selection === "View error message") { - let outputJson = JSON.parse(TestRunner.getJsonFromOutput(stdout)); - let outputChannel = vscode.window.createOutputChannel('Ruby Test Explorer Error Message'); - - if (outputJson.messages.length > 0) { - let outputJsonString = outputJson.messages.join("\n\n"); - let outputJsonArray = outputJsonString.split("\n"); - outputJsonArray.forEach((line: string) => { - outputChannel.appendLine(line); - }) - } else { - outputChannel.append(err.message); - } - outputChannel.show(false); - } - }); - - reject(err); - } - } - resolve(stdout); - }); - }); - return await output - }; - /** * Handles test state based on the output returned by the custom RSpec formatter. * @@ -132,27 +71,4 @@ export class RspecTestRunner extends TestRunner { context.skipped(testItem) } }; - - protected getListTestsCommand(testItems?: vscode.TestItem[]): string { - let cfg = this.config as RspecConfig - let cmd = `${cfg.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; - - testItems?.forEach((item) => { - let testPath = `${cfg.getAbsoluteTestDirectory()}${path.sep}${item.id}` - cmd = `${cmd} "${testPath}"` - }) - return cmd - } - - protected getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` - }; - - protected getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string { - return `${(this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig)} '${context.config.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` - }; - - protected getFullTestSuiteCommand(context: TestRunContext): string { - return (this.config as RspecConfig).testCommandWithFormatterAndDebugger(context.debuggerConfig) - }; } diff --git a/src/testFactory.ts b/src/testFactory.ts index f49adea..8b52222 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -19,6 +19,7 @@ export class TestFactory implements vscode.Disposable { private readonly log: IVSCodeExtLogger, private readonly controller: vscode.TestController, private config: Config, + private readonly profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile }, private readonly workspace?: vscode.WorkspaceFolder, ) { this.disposables.push(this.configWatcher()); @@ -60,7 +61,7 @@ export class TestFactory implements vscode.Disposable { this.loader = new TestLoader( this.log, this.controller, - this.getRunner(), + this.profiles.resolveTestsProfile, this.config, this.testSuite ) diff --git a/src/testLoader.ts b/src/testLoader.ts index e2a58f9..d7a571d 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,7 +1,5 @@ import * as vscode from 'vscode'; import { IChildLogger } from '@vscode-logging/logger'; -import { RspecTestRunner } from './rspec/rspecTestRunner'; -import { MinitestTestRunner } from './minitest/minitestTestRunner'; import { Config } from './config'; import { TestSuite } from './testSuite'; @@ -31,11 +29,12 @@ export type ParsedTest = { export class TestLoader implements vscode.Disposable { protected disposables: { dispose(): void }[] = []; private readonly log: IChildLogger; + private readonly cancellationTokenSource = new vscode.CancellationTokenSource() constructor( readonly rootLog: IChildLogger, private readonly controller: vscode.TestController, - private readonly testRunner: RspecTestRunner | MinitestTestRunner, + private readonly resolveTestProfile: vscode.TestRunProfile, private readonly config: Config, private readonly testSuite: TestSuite, ) { @@ -115,10 +114,8 @@ export class TestLoader implements vscode.Disposable { let log = this.log.getChildLogger({label:"loadTests"}) log.info(`Loading tests for ${testItems.length} items (${this.config.frameworkName()})...`); try { - let output = await this.testRunner.initTests(testItems); - - log.debug(`Passing raw output from dry-run into getJsonFromOutput: ${output}`); - this.testRunner.parseAndHandleTestOutput(output) + let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) + await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) } catch (e: any) { log.error("Failed to load tests", e) return Promise.reject(e) diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 9ff78ab..e77824a 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' import { IChildLogger } from '@vscode-logging/logger' -import { Config } from './config' /** * Test run context @@ -25,7 +24,6 @@ export class TestRunContext { public readonly token: vscode.CancellationToken, readonly request: vscode.TestRunRequest, readonly controller: vscode.TestController, - public readonly config: Config, public readonly debuggerConfig?: vscode.DebugConfiguration ) { this.log = rootLog.getChildLogger({ label: "TestRunContext" }) diff --git a/src/testRunner.ts b/src/testRunner.ts index 4bd6229..eb9c45d 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -24,7 +24,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param testSuite TestSuite instance */ constructor( - rootLog: IChildLogger, + readonly rootLog: IChildLogger, protected controller: vscode.TestController, protected config: RspecConfig | MinitestConfig, protected testSuite: TestSuite, @@ -33,12 +33,6 @@ export abstract class TestRunner implements vscode.Disposable { this.log = rootLog.getChildLogger({label: "TestRunner"}) } - /** - * Initialise the test framework, parse tests (without executing) and retrieve the output - * @return Stdout outpu from framework initialisation - */ - abstract initTests(testItems: vscode.TestItem[]): Promise - abstract canNotifyOnStartingTests: boolean public dispose() { @@ -119,7 +113,7 @@ export abstract class TestRunner implements vscode.Disposable { */ async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; - let log = this.log.getChildLogger({ label: `ChildProcess(${context.config.frameworkName()})` }) + let log = this.log.getChildLogger({ label: `ChildProcess(${this.config.frameworkName()})` }) let testIdsRun: vscode.TestItem[] = [] process.stderr!.pipe(split2()).on('data', (data) => { @@ -159,11 +153,8 @@ export abstract class TestRunner implements vscode.Disposable { await new Promise((resolve, reject) => { process.once('exit', (code: number, signal: string) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Exit with error, code: ${code}, signal: ${signal}`)); - } + log.debug('Child process exited', code, signal) + resolve(); }); process.once('error', (err: Error) => { reject(err); @@ -187,12 +178,10 @@ export abstract class TestRunner implements vscode.Disposable { debuggerConfig?: vscode.DebugConfiguration ) { const context = new TestRunContext( - this.log, + this.rootLog, token, request, - this.controller, - this.config, - debuggerConfig + this.controller ) let log = this.log.getChildLogger({ label: `runHandler` }) try { @@ -313,13 +302,16 @@ export abstract class TestRunner implements vscode.Disposable { // with runFullTestSuite() try { let testsRun: vscode.TestItem[] - if (node == null) { + let command: string + if (context.request.profile?.label === 'ResolveTests') { + command = this.config.getResolveTestsCommand(context.request.include) + } else if (node == null) { log.debug("Running all tests") this.controller.items.forEach((testSuite) => { // Mark selected tests as started this.enqueTestAndChildren(testSuite, context) }) - testsRun = await this.runFullTestSuite(context) + command = this.config.getFullTestSuiteCommand(context.debuggerConfig) } else if (node.canResolveChildren) { // If the suite is a file, run the tests as a file rather than as separate tests. log.debug(`Running test file/folder: ${node.id}`) @@ -327,7 +319,7 @@ export abstract class TestRunner implements vscode.Disposable { // Mark selected tests as started this.enqueTestAndChildren(node, context) - testsRun = await this.runTestFile(context, node) + command = this.config.getTestFileCommand(node, context.debuggerConfig) } else { if (node.uri !== undefined) { log.debug(`Running single test: ${node.id}`) @@ -335,12 +327,13 @@ export abstract class TestRunner implements vscode.Disposable { // Run the test at the given line, add one since the line is 0-indexed in // VS Code and 1-indexed for RSpec/Minitest. - testsRun = await this.runSingleTest(context, node); + command = this.config.getSingleTestCommand(node, context.debuggerConfig) } else { log.error("test missing file path") return } } + testsRun = await this.runTestFramework(command, context) this.testSuite.removeMissingTests(testsRun, node) } finally { context.testRun.end() @@ -440,20 +433,6 @@ export abstract class TestRunner implements vscode.Disposable { } } - /** - * Runs the test framework with the given command. - * - * @param testCommand Command to use to run the test framework - * @param type Type of test run for logging (full, single file, single test) - * @param context Test run context - * @return The raw output from running the test suite. - */ - async runTestFramework(testCommand: string, type: string, context: TestRunContext): Promise { - this.log.trace(`Running test suite: ${type}`); - - return await this.spawnCancellableChild(testCommand, context) - }; - /** * Spawns a child process to run a command, that will be killed * if the cancellation token is triggered @@ -462,7 +441,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - protected async spawnCancellableChild (testCommand: string, context: TestRunContext): Promise { + protected async runTestFramework (testCommand: string, context: TestRunContext): Promise { context.token.onCancellationRequested(() => { this.log.debug("Cancellation requested") this.killChild() @@ -471,7 +450,7 @@ export abstract class TestRunner implements vscode.Disposable { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, - env: context.config.getProcessEnv() + env: this.config.getProcessEnv() }; this.log.debug(`Running command: ${testCommand}`); @@ -484,85 +463,6 @@ export abstract class TestRunner implements vscode.Disposable { return await this.handleChildProcess(testProcess, context); } - /** - * Runs a single test. - * - * @param testLocation A file path with a line number, e.g. `/path/to/test.rb:12`. - * @param context Test run context - * @return The raw output from running the test. - */ - protected async runSingleTest(context: TestRunContext, testItem: vscode.TestItem): Promise { - this.log.info(`Running single test: ${testItem.id}`); - return await this.runTestFramework( - this.getSingleTestCommand(testItem, context), - "single test", - context) - } - - /** - * Runs tests in a given file. - * - * @param testFile The test file's file path, e.g. `/path/to/test.rb`. - * @param context Test run context - * @return The raw output from running the tests. - */ - protected async runTestFile(context: TestRunContext, testItem: vscode.TestItem): Promise { - this.log.info(`Running test file: ${testItem.id}`); - return await this.runTestFramework( - this.getTestFileCommand(testItem, context), - "test file", - context) - } - - /** - * Runs the full test suite for the current workspace. - * - * @param context Test run context - * @return The raw output from running the test suite. - */ - protected async runFullTestSuite(context: TestRunContext): Promise { - this.log.info(`Running full test suite.`); - return await this.runTestFramework( - this.getFullTestSuiteCommand(context), - "all tests", - context) - } - - /** - * Gets the command to get information about tests. - * - * @param testItems Optional array of tests to list. If missing, all tests should be - * listed - * @return The command to run to get test details - */ - protected abstract getListTestsCommand(testItems?: vscode.TestItem[]): string; - - /** - * Gets the command to run a single test. - * - * @param testItem A TestItem of a single test to be run - * @param context Test run context - * @return The command to run a single test. - */ - protected abstract getSingleTestCommand(testItem: vscode.TestItem, context: TestRunContext): string; - - /** - * Gets the command to run tests in a given file. - * - * @param testItem A TestItem of a file containing tests - * @param context Test run context - * @return The command to run all tests in a given file. - */ - protected abstract getTestFileCommand(testItem: vscode.TestItem, context: TestRunContext): string; - - /** - * Gets the command to run the full test suite for the current workspace. - * - * @param context Test run context - * @return The command to run the full test suite for the current workspace. - */ - protected abstract getFullTestSuiteCommand(context: TestRunContext): string; - /** * Handles test state based on the output returned by the test command. * diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index b749027..9a6c629 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as path from 'path' -import { anything, instance, verify } from 'ts-mockito' +import { anything, instance, verify, mock, when } from 'ts-mockito' import { expect } from 'chai'; import { before, beforeEach } from 'mocha'; @@ -19,6 +19,7 @@ suite('Extension Test for Minitest', function() { let testRunner: MinitestTestRunner; let testLoader: TestLoader; let testSuite: TestSuite; + let resolveTestsProfile: vscode.TestRunProfile; let expectedPath = (file: string): string => { return path.resolve( @@ -64,6 +65,10 @@ suite('Extension Test for Minitest', function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) + let mockProfile = mock() + when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.label).thenReturn('ResolveTests') + resolveTestsProfile = instance(mockProfile) }) suite('dry run', function() { @@ -71,7 +76,7 @@ suite('Extension Test for Minitest', function() { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(stdout_logger("debug"), testController, config) testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -198,7 +203,7 @@ suite('Extension Test for Minitest', function() { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 856ddc1..014477c 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as path from 'path' -import { anything, instance, verify } from 'ts-mockito' +import { anything, instance, verify, mock, when } from 'ts-mockito' import { before, beforeEach } from 'mocha'; import { expect } from 'chai'; @@ -19,6 +19,7 @@ suite('Extension Test for RSpec', function() { let testRunner: RspecTestRunner; let testLoader: TestLoader; let testSuite: TestSuite; + let resolveTestsProfile: vscode.TestRunProfile; let expectedPath = (file: string): string => { return path.resolve( @@ -71,7 +72,11 @@ suite('Extension Test for RSpec', function() { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(noop_logger(), testController, config) testRunner = new RspecTestRunner(noop_logger(), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + let mockProfile = mock() + when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.label).thenReturn('ResolveTests') + resolveTestsProfile = instance(mockProfile) + testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -187,13 +192,17 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(noop_logger(), testController, config) + testSuite = new TestSuite(stdout_logger(), testController, config) testRunner = new RspecTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite); + let mockProfile = mock() + when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.label).thenReturn('ResolveTests') + resolveTestsProfile = instance(mockProfile) + testLoader = new TestLoader(stdout_logger(), testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) - suite(`running collections emits correct statuses`, async function() { + suite.only(`running collections emits correct statuses`, async function() { let mockTestRun: vscode.TestRun test('when running full suite', async function() { From 57657b528fd4dbdaae003a8e67b36193a72dfb61 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 5 Jan 2023 23:52:37 +0000 Subject: [PATCH 053/108] Get everything more-or-less working again --- src/minitest/minitestConfig.ts | 8 +- src/testLoader.ts | 14 +-- src/testRunContext.ts | 20 ++-- src/testRunner.ts | 140 +++++++++++++++---------- src/testSuite.ts | 30 +++--- test/suite/minitest/minitest.test.ts | 8 +- test/suite/rspec/rspec.test.ts | 19 ++-- test/suite/unitTests/testSuite.test.ts | 26 ++++- 8 files changed, 160 insertions(+), 105 deletions(-) diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index b44523b..c6603c6 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -58,14 +58,12 @@ export class MinitestConfig extends Config { } public getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - let line = testItem.id.split(':').pop(); - let relativeLocation = path.join(this.getAbsoluteTestDirectory(), testItem.id) - return `${this.testCommandWithDebugger(debugConfiguration)} '${relativeLocation}:${line}'` + let line = testItem.range!.start.line + 1 + return `${this.testCommandWithDebugger(debugConfiguration)} '${testItem.uri?.fsPath}:${line}'` }; public getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - let relativeFile = path.join(this.getAbsoluteTestDirectory(), testItem.id) - return `${this.testCommandWithDebugger(debugConfiguration)} '${relativeFile}'` + return `${this.testCommandWithDebugger(debugConfiguration)} '${testItem.uri?.fsPath}'` }; public getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string { diff --git a/src/testLoader.ts b/src/testLoader.ts index d7a571d..48764d9 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -56,7 +56,7 @@ export class TestLoader implements vscode.Disposable { * - A test file is deleted */ async createWatcher(pattern: vscode.GlobPattern): Promise { - let log = this.log.getChildLogger({label: `createWatcher(${pattern})`}) + let log = this.log.getChildLogger({label: `createWatcher(${pattern.toString()})`}) const watcher = vscode.workspace.createFileSystemWatcher(pattern); // When files are created, make sure there's a corresponding "file" node in the tree @@ -77,11 +77,13 @@ export class TestLoader implements vscode.Disposable { this.testSuite.deleteTestItem(uri) }); - let testFiles = [] for (const file of await vscode.workspace.findFiles(pattern)) { - testFiles.push(this.testSuite.getOrCreateTestItem(file)) + log.debug("Found file, creating TestItem", file) + this.testSuite.getOrCreateTestItem(file) } - await this.loadTests(testFiles) + + log.debug("Resolving tests in found files") + await this.loadTests() return watcher; } @@ -110,9 +112,9 @@ export class TestLoader implements vscode.Disposable { * * @return The full test suite. */ - public async loadTests(testItems: vscode.TestItem[]): Promise { + public async loadTests(testItems?: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItems.length} items (${this.config.frameworkName()})...`); + log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.config.frameworkName()})...`); try { let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) diff --git a/src/testRunContext.ts b/src/testRunContext.ts index e77824a..140f685 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -26,7 +26,8 @@ export class TestRunContext { readonly controller: vscode.TestController, public readonly debuggerConfig?: vscode.DebugConfiguration ) { - this.log = rootLog.getChildLogger({ label: "TestRunContext" }) + this.log = rootLog.getChildLogger({ label: `TestRunContext(${request.profile?.label})` }) + this.log.info('Creating test run'); this.testRun = controller.createTestRun(request) } @@ -36,8 +37,8 @@ export class TestRunContext { * @param test Test item to update. */ public enqueued(test: vscode.TestItem): void { - this.testRun.enqueued(test) this.log.debug(`Enqueued: ${test.id}`) + this.testRun.enqueued(test) } /** @@ -63,9 +64,9 @@ export class TestRunContext { testItem.uri ?? vscode.Uri.file(file), new vscode.Position(line, 0) ) - this.testRun.errored(testItem, testMessage, duration) this.log.debug(`Errored: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) this.log.trace(`Error message: ${message}`) + this.testRun.errored(testItem, testMessage, duration) } catch (e: any) { this.log.error(`Failed to set test ${test} as Errored`, e) } @@ -92,9 +93,9 @@ export class TestRunContext { test.uri ?? vscode.Uri.file(file), new vscode.Position(line, 0) ) - this.testRun.failed(test, testMessage, duration) this.log.debug(`Failed: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) this.log.trace(`Failure message: ${message}`) + this.testRun.failed(test, testMessage, duration) } /** @@ -106,8 +107,8 @@ export class TestRunContext { public passed(test: vscode.TestItem, duration?: number | undefined ): void { - this.testRun.passed(test, duration) this.log.debug(`Passed: ${test.id}${duration ? `, duration: ${duration}ms` : ''}`) + this.testRun.passed(test, duration) } /** @@ -116,8 +117,8 @@ export class TestRunContext { * @param test ID of the test item to update, or the test item. */ public skipped(test: vscode.TestItem): void { - this.testRun.skipped(test) this.log.debug(`Skipped: ${test.id}`) + this.testRun.skipped(test) } /** @@ -126,7 +127,12 @@ export class TestRunContext { * @param test Test item to update, or the test item. */ public started(test: vscode.TestItem): void { - this.testRun.started(test) this.log.debug(`Started: ${test.id}`) + this.testRun.started(test) + } + + public endTestRun(): void { + this.log.info('Ending test run'); + this.testRun.end() } } diff --git a/src/testRunner.ts b/src/testRunner.ts index eb9c45d..fdb7482 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -114,7 +114,6 @@ export abstract class TestRunner implements vscode.Disposable { async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; let log = this.log.getChildLogger({ label: `ChildProcess(${this.config.frameworkName()})` }) - let testIdsRun: vscode.TestItem[] = [] process.stderr!.pipe(split2()).on('data', (data) => { data = data.toString(); @@ -124,6 +123,7 @@ export abstract class TestRunner implements vscode.Disposable { } }) + let parsedTests: vscode.TestItem[] = [] process.stdout!.pipe(split2()).on('data', (data) => { let getTest = (testId: string): vscode.TestItem => { testId = this.testSuite.normaliseTestId(testId) @@ -145,23 +145,27 @@ export abstract class TestRunner implements vscode.Disposable { context.skipped(getTest(data.replace('PENDING: ', ''))) } else if (data.includes('START_OF_TEST_JSON')) { log.trace("Received test run results", data); - testIdsRun = testIdsRun.concat(this.parseAndHandleTestOutput(data, context)); + parsedTests = this.parseAndHandleTestOutput(data, context); } else { log.trace("Ignoring unrecognised output", data) } }); - await new Promise((resolve, reject) => { + await new Promise<{code:number, signal:string}>((resolve, reject) => { process.once('exit', (code: number, signal: string) => { - log.debug('Child process exited', code, signal) - resolve(); + log.trace('Child process exited', code, signal) + }); + process.once('close', (code: number, signal: string) => { + log.debug('Child process exited, and all streams closed', code, signal) + resolve({code, signal}); }); process.once('error', (err: Error) => { + log.debug('Error event from child process', err.message) reject(err); }); }) - return testIdsRun + return parsedTests }; /** @@ -177,55 +181,57 @@ export abstract class TestRunner implements vscode.Disposable { token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration ) { - const context = new TestRunContext( - this.rootLog, - token, - request, - this.controller - ) let log = this.log.getChildLogger({ label: `runHandler` }) - try { - const queue: vscode.TestItem[] = []; - if (debuggerConfig) { - log.debug(`Debugging test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); - - if (!this.workspace) { - log.error("Cannot debug without a folder opened") - context.testRun.end() - return - } + const queue: { context: TestRunContext, test: vscode.TestItem }[] = []; - this.log.info('Starting the debug session'); - let debugSession: any; - try { - await this.debugCommandStarted() - debugSession = await this.startDebugging(debuggerConfig); - } catch (err) { - log.error('Failed starting the debug session - aborting', err); - this.killChild(); - return; - } + if (debuggerConfig) { + log.debug(`Debugging test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); - const subscription = this.onDidTerminateDebugSession((session) => { - if (debugSession != session) return; - log.info('Debug session ended'); - this.killChild(); // terminate the test run - subscription.dispose(); - }); + if (this.workspace) { + log.error("Cannot debug without a folder opened") + return } - else { - log.debug(`Running test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); + + this.log.info('Starting the debug session'); + let debugSession: any; + try { + await this.debugCommandStarted() + debugSession = await this.startDebugging(debuggerConfig); + } catch (err) { + log.error('Failed starting the debug session - aborting', err); + this.killChild(); + return; } - // Loop through all included tests, or all known tests, and add them to our queue - if (request.include) { - log.trace(`${request.include.length} tests in request`); - request.include.forEach(test => queue.push(test)); + const subscription = this.onDidTerminateDebugSession((session) => { + if (debugSession != session) return; + log.info('Debug session ended'); + this.killChild(); // terminate the test run + subscription.dispose(); + }); + } + else { + log.debug(`Running test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); + } - // For every test that was queued, try to run it - while (queue.length > 0 && !token.isCancellationRequested) { - const test = queue.pop()!; + // Loop through all included tests, or all known tests, and add them to our queue + if (request.include) { + log.debug(`${request.include.length} tests in request`); + request.include.forEach(test => queue.push({ + context: new TestRunContext( + this.rootLog, + token, + request, + this.controller + ), + test: test, + })); + + // For every test that was queued, try to run it + while (queue.length > 0 && !token.isCancellationRequested) { + const {context, test} = queue.pop()!; + try { log.trace(`Running test from queue ${test.id}`); // Skip tests the user asked to exclude @@ -236,17 +242,37 @@ export abstract class TestRunner implements vscode.Disposable { await this.runNode(context, test); } + catch (err) { + log.error("Error running tests", err) + } + finally { + // Make sure to end the run after all tests have been executed: + log.info('Ending test run'); + context.endTestRun(); + } if (token.isCancellationRequested) { log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } - } else { - log.trace('Running all tests in suite'); + } + } else { + log.debug('Running all tests in suite'); + const context = new TestRunContext( + this.rootLog, + token, + request, + this.controller + ) + try { await this.runNode(context); } - } - finally { - // Make sure to end the run after all tests have been executed: - context.testRun.end(); + catch (err) { + log.error("Error running tests", err) + } + finally { + // Make sure to end the run after all tests have been executed: + log.info('Ending test run'); + context.endTestRun(); + } } } @@ -304,7 +330,7 @@ export abstract class TestRunner implements vscode.Disposable { let testsRun: vscode.TestItem[] let command: string if (context.request.profile?.label === 'ResolveTests') { - command = this.config.getResolveTestsCommand(context.request.include) + command = this.config.getResolveTestsCommand(node ? [node] : context.request.include) } else if (node == null) { log.debug("Running all tests") this.controller.items.forEach((testSuite) => { @@ -334,7 +360,9 @@ export abstract class TestRunner implements vscode.Disposable { } } testsRun = await this.runTestFramework(command, context) - this.testSuite.removeMissingTests(testsRun, node) + if (context.request.profile?.label === 'ResolveTests') { + this.testSuite.removeMissingTests(testsRun, node) + } } finally { context.testRun.end() } @@ -387,7 +415,7 @@ export abstract class TestRunner implements vscode.Disposable { newTestItem.label = description newTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); parsedTests.push(newTestItem) - log.trace("Parsed test", test) + log.debug("Parsed test", test) if(context) { // Only handle status if actual test run, not dry run this.handleStatus(test, context); diff --git a/src/testSuite.ts b/src/testSuite.ts index b11b5f2..9545eeb 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -90,23 +90,19 @@ export class TestSuite { } public removeMissingTests(parsedTests: vscode.TestItem[], parent?: vscode.TestItem) { - //let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) - let collection = parent?.children || this.controller.items - collection.forEach(item => { - if (!item.canResolveChildren) { - //log.debug(`Not an item with children - returning`) - return - } - //log.debug(`Checking tests in ${item.id}`) - if (parsedTests.find(x => x.id == item.id)) { - //log.debug(`Parsed test contains ${item.id}`) - let filteredTests = parsedTests.filter(x => x.parent?.id == item.id) - this.removeMissingTests(filteredTests, item) - } else { - //log.debug(`Parsed tests don't contain ${item.id}. Deleting`) - collection.delete(item.id) - } - }) + let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) + + log.debug("Tests to check", parsedTests.length) + while (parsedTests.length > 0) { + let parent = parsedTests[0].parent + log.debug("Checking parent", parent?.id) + let parentCollection = parent ? parent.children : this.controller.items + let parentCollectionSize = parentCollection.size + parentCollection.replace(parsedTests.filter(x => x.parent == parent)) + log.debug("Removed tests from parent", parentCollectionSize - parentCollection.size) + parsedTests = parsedTests.filter(x => x.parent != parent) + log.debug("Remaining tests to check", parsedTests.length) + } } /** diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 9a6c629..3fd1fb4 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -66,7 +66,7 @@ suite('Extension Test for Minitest', function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) let mockProfile = mock() - when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) }) @@ -201,8 +201,8 @@ suite('Extension Test for Minitest', function() { before(async function() { testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) + testSuite = new TestSuite(stdout_logger("debug"), testController, config) + testRunner = new MinitestTestRunner(stdout_logger("trace"), testController, config, testSuite, workspaceFolder) testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) @@ -276,7 +276,7 @@ suite('Extension Test for Minitest', function() { status: "errored", expectedTest: abs_zero_expectation, failureExpectation: { - message: "RuntimeError:\nAbs for zero is not supported", + message: "RuntimeError: Abs for zero is not supported", line: 8, } }, diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 014477c..76a820e 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -9,7 +9,7 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { noop_logger, stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; +import { stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -69,14 +69,14 @@ suite('Extension Test for RSpec', function() { suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(noop_logger(), testController, config) - testRunner = new RspecTestRunner(noop_logger(), testController, config, testSuite, workspaceFolder) + testController = new StubTestController(stdout_logger("debug")) + testSuite = new TestSuite(stdout_logger("debug"), testController, config) + testRunner = new RspecTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) let mockProfile = mock() - when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) - testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); + testLoader = new TestLoader(stdout_logger("debug"), testController, resolveTestsProfile, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -147,6 +147,7 @@ suite('Extension Test for RSpec', function() { }) test('Load all tests', async function () { + console.log("resolving files in test") await testLoader.discoverAllFilesInWorkspace() const testSuite = testController.items @@ -193,16 +194,16 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(stdout_logger()) testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) + testRunner = new RspecTestRunner(stdout_logger("trace"), testController, config, testSuite, workspaceFolder) let mockProfile = mock() - when(mockProfile.runHandler).thenReturn(testRunner.runHandler) + when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) testLoader = new TestLoader(stdout_logger(), testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) - suite.only(`running collections emits correct statuses`, async function() { + suite(`running collections emits correct statuses`, async function() { let mockTestRun: vscode.TestRun test('when running full suite', async function() { diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index fd9e61d..c7e01c0 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -160,7 +160,7 @@ suite('TestSuite', function () { testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) }) - test('creates item and parent if parent of nested ID is not found', function () { + test('creates item and parent if parent of nested file is not found', function () { let id = `folder${path.sep}not-found` let testItem = testSuite.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined @@ -171,5 +171,29 @@ suite('TestSuite', function () { expect(folder?.children.get(id)).to.eq(testItem) testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) }) + + suite('creates full item tree for specs within files', function () { + let fileId = `folder${path.sep}not-found.rb` + + for (const {suite, location} of [ + {suite: 'minitest', location: '[4]'}, + {suite: 'rspec', location: '[1:2]'}, + ]) { + test(suite, function() { + let id = `${fileId}${location}` + let testItem = testSuite.getOrCreateTestItem(id) + expect(testItem.id).to.eq(id) + expect(testItem.parent?.id).to.eq(fileId) + + let folderItem = testSuite.getTestItem('folder') + let fileItem = testSuite.getTestItem(fileId) + expect(folderItem?.children.size).to.eq(1) + expect(fileItem?.children.size).to.eq(1) + testUriMatches(folderItem!, path.resolve(config.getAbsoluteTestDirectory(), 'folder')) + testUriMatches(fileItem!, path.resolve(config.getAbsoluteTestDirectory(), fileId)) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), fileId)) + }) + } + }) }) }); From 8b1347b335dd777e102f31dcb5edb43482d4136a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 03:09:23 +0000 Subject: [PATCH 054/108] Simplify TestRunner, fix removeMissingTests logic & bugs --- src/testRunContext.ts | 2 +- src/testRunner.ts | 162 +++++++++------------------ src/testSuite.ts | 21 ++-- test/suite/helpers.ts | 30 ++++- test/suite/minitest/minitest.test.ts | 30 +++-- test/suite/rspec/rspec.test.ts | 30 +++-- 6 files changed, 123 insertions(+), 152 deletions(-) diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 140f685..1c26415 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -132,7 +132,7 @@ export class TestRunContext { } public endTestRun(): void { - this.log.info('Ending test run'); + this.log.debug('Ending test run'); this.testRun.end() } } diff --git a/src/testRunner.ts b/src/testRunner.ts index fdb7482..339dac9 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -216,63 +216,62 @@ export abstract class TestRunner implements vscode.Disposable { } // Loop through all included tests, or all known tests, and add them to our queue - if (request.include) { - log.debug(`${request.include.length} tests in request`); - request.include.forEach(test => queue.push({ - context: new TestRunContext( - this.rootLog, - token, - request, - this.controller - ), - test: test, - })); - - // For every test that was queued, try to run it - while (queue.length > 0 && !token.isCancellationRequested) { - const {context, test} = queue.pop()!; - try { - log.trace(`Running test from queue ${test.id}`); - - // Skip tests the user asked to exclude - if (request.exclude?.includes(test)) { - log.debug(`Skipping test excluded from test run: ${test.id}`) - continue; - } + log.debug(`${request.include?.length || 0} tests in request`); + let context = new TestRunContext( + this.rootLog, + token, + request, + this.controller + ); - await this.runNode(context, test); - } - catch (err) { - log.error("Error running tests", err) - } - finally { - // Make sure to end the run after all tests have been executed: - log.info('Ending test run'); - context.endTestRun(); - } - if (token.isCancellationRequested) { - log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + try { + log.trace("Included tests in request", request.include?.map(x => x.id)); + log.trace("Excluded tests in request", request.exclude?.map(x => x.id)); + let testsToRun = request.exclude ? request.include?.filter(x => !request.exclude!.includes(x)) : request.include + log.trace("Running tests", testsToRun?.map(x => x.id)); + + let command: string + if (context.request.profile?.label === 'ResolveTests') { + command = this.config.getResolveTestsCommand(testsToRun) + let testsRun = await this.runTestFramework(command, context) + this.testSuite.removeMissingTests(testsRun, testsToRun) + } else if (!testsToRun) { + log.debug("Running all tests") + this.controller.items.forEach((testSuite) => { + // Mark selected tests as started + this.enqueTestAndChildren(testSuite, context) + }) + command = this.config.getFullTestSuiteCommand(context.debuggerConfig) + } else { + log.debug("Running selected tests") + command = this.config.getFullTestSuiteCommand(context.debuggerConfig) + for (const node of testsToRun) { + log.trace("Adding test to command", node.id) + // Mark selected tests as started + this.enqueTestAndChildren(node, context) + command = `${command} ${node.uri?.fsPath}` + if (!node.canResolveChildren) { + // single test + if (!node.range) { + throw new Error(`Test item is missing line number: ${node.id}`) + } + command = `${command}:${node.range!.start.line + 1}` + } + log.trace("Current command", command) } } - } else { - log.debug('Running all tests in suite'); - const context = new TestRunContext( - this.rootLog, - token, - request, - this.controller - ) - try { - await this.runNode(context); - } - catch (err) { - log.error("Error running tests", err) - } - finally { - // Make sure to end the run after all tests have been executed: - log.info('Ending test run'); - context.endTestRun(); - } + await this.runTestFramework(command, context) + } + catch (err) { + log.error("Error running tests", err) + } + finally { + // Make sure to end the run after all tests have been executed: + log.info('Ending test run'); + context.endTestRun(); + } + if (token.isCancellationRequested) { + log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) } } @@ -313,61 +312,6 @@ export abstract class TestRunner implements vscode.Disposable { return vscode.debug.onDidTerminateDebugSession(cb); } - /** - * Recursively run a node or its children. - * - * @param node A test or test suite. - * @param context Test run context - */ - protected async runNode( - context: TestRunContext, - node?: vscode.TestItem, - ): Promise { - let log = this.log.getChildLogger({label: this.runNode.name}) - // Special case handling for the root suite, since it can be run - // with runFullTestSuite() - try { - let testsRun: vscode.TestItem[] - let command: string - if (context.request.profile?.label === 'ResolveTests') { - command = this.config.getResolveTestsCommand(node ? [node] : context.request.include) - } else if (node == null) { - log.debug("Running all tests") - this.controller.items.forEach((testSuite) => { - // Mark selected tests as started - this.enqueTestAndChildren(testSuite, context) - }) - command = this.config.getFullTestSuiteCommand(context.debuggerConfig) - } else if (node.canResolveChildren) { - // If the suite is a file, run the tests as a file rather than as separate tests. - log.debug(`Running test file/folder: ${node.id}`) - - // Mark selected tests as started - this.enqueTestAndChildren(node, context) - - command = this.config.getTestFileCommand(node, context.debuggerConfig) - } else { - if (node.uri !== undefined) { - log.debug(`Running single test: ${node.id}`) - this.enqueTestAndChildren(node, context) - - // Run the test at the given line, add one since the line is 0-indexed in - // VS Code and 1-indexed for RSpec/Minitest. - command = this.config.getSingleTestCommand(node, context.debuggerConfig) - } else { - log.error("test missing file path") - return - } - } - testsRun = await this.runTestFramework(command, context) - if (context.request.profile?.label === 'ResolveTests') { - this.testSuite.removeMissingTests(testsRun, node) - } - } finally { - context.testRun.end() - } - } - public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext): vscode.TestItem[] { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); diff --git a/src/testSuite.ts b/src/testSuite.ts index 9545eeb..c50e307 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -89,17 +89,24 @@ export class TestSuite { return testItem } - public removeMissingTests(parsedTests: vscode.TestItem[], parent?: vscode.TestItem) { + public removeMissingTests(parsedTests: vscode.TestItem[], requestedTests?: readonly vscode.TestItem[]) { let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) - log.debug("Tests to check", parsedTests.length) + log.debug("Tests to check", JSON.stringify(parsedTests.map(x => x.id))) + + // Files and folders are removed by the filesystem watchers so we only need to clear out single tests + parsedTests = parsedTests.filter(x => !x.canResolveChildren && x.parent) while (parsedTests.length > 0) { let parent = parsedTests[0].parent - log.debug("Checking parent", parent?.id) - let parentCollection = parent ? parent.children : this.controller.items - let parentCollectionSize = parentCollection.size - parentCollection.replace(parsedTests.filter(x => x.parent == parent)) - log.debug("Removed tests from parent", parentCollectionSize - parentCollection.size) + if (!requestedTests || requestedTests.includes(parent!)) { + // If full suite was resolved we can always replace. If partial suite was resolved, we should + // only replace children if parent was resolved, else we might remove tests that do exist + log.debug("Checking parent", parent?.id) + let parentCollection = parent ? parent.children : this.controller.items + let parentCollectionSize = parentCollection.size + parentCollection.replace(parsedTests.filter(x => x.parent == parent)) + log.debug("Removed tests from parent", parentCollectionSize - parentCollection.size) + } parsedTests = parsedTests.filter(x => x.parent != parent) log.debug("Remaining tests to check", parsedTests.length) } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 1e10734..c5afc3f 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -59,14 +59,38 @@ export function stdout_logger(level: LogLevel = "info"): IVSCodeExtLogger { "debug": 4, "trace": 5, } + const divider = '----------' let maxLevel = levels[level] function writeStdOutLogMsg(level: LogLevel, msg: string, ...args: any[]): void { if (levels[level] <= maxLevel) { - console.log(`[${level}] ${msg}${args.length > 0 ? ':' : ''}`) + let message = `[${level}] ${msg}${args.length > 0 ? ':' : ''}` args.forEach((arg) => { - console.log(`${JSON.stringify(arg)}`) + if (arg instanceof Error) { + message = `${message}\n ${arg.stack ? arg.stack : arg.name + ': ' + arg.message}` + } else { + message = `${message}\n ${JSON.stringify(arg)}` + } }) - console.log('----------') + switch(level) { + case "fatal": + case "error": + console.error(message) + console.error(divider) + break; + case "warn": + console.warn(message) + console.warn(divider) + break; + case "info": + console.info(message) + console.info(divider) + break; + case "debug": + case "trace": + console.debug(message) + console.debug(divider) + break; + } } } let logger: IVSCodeExtLogger = { diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 3fd1fb4..7f18fac 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -9,7 +9,7 @@ import { TestSuite } from '../../../src/testSuite'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { noop_logger, setupMockRequest, stdout_logger, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; +import { stdout_logger, setupMockRequest, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { @@ -21,6 +21,9 @@ suite('Extension Test for Minitest', function() { let testSuite: TestSuite; let resolveTestsProfile: vscode.TestRunProfile; + //const logger = noop_logger(); + const logger = stdout_logger("info"); + let expectedPath = (file: string): string => { return path.resolve( 'test', @@ -73,10 +76,10 @@ suite('Extension Test for Minitest', function() { suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(stdout_logger("debug"), testController, config) - testRunner = new MinitestTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(logger) + testSuite = new TestSuite(logger, testController, config) + testRunner = new MinitestTestRunner(logger, testController, config, testSuite, workspaceFolder) + testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -200,10 +203,10 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource(); before(async function() { - testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(stdout_logger("debug"), testController, config) - testRunner = new MinitestTestRunner(stdout_logger("trace"), testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(noop_logger(), testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(logger) + testSuite = new TestSuite(logger, testController, config) + testRunner = new MinitestTestRunner(logger, testController, config, testSuite, workspaceFolder) + testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) @@ -286,16 +289,11 @@ suite('Extension Test for Minitest', function() { } ] for(const {status, expectedTest, failureExpectation} of params) { - let mockTestRun: vscode.TestRun - - beforeEach(async function() { + test(`id: ${expectedTest.id}, status: ${status}`, async function() { let mockRequest = setupMockRequest(testSuite, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! - }) - - test(`id: ${expectedTest.id}, status: ${status}`, function() { + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! switch(status) { case "passed": testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 76a820e..91fff42 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -21,6 +21,9 @@ suite('Extension Test for RSpec', function() { let testSuite: TestSuite; let resolveTestsProfile: vscode.TestRunProfile; + //const logger = noop_logger(); + const logger = stdout_logger("info"); + let expectedPath = (file: string): string => { return path.resolve( 'test', @@ -69,14 +72,14 @@ suite('Extension Test for RSpec', function() { suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(stdout_logger("debug")) - testSuite = new TestSuite(stdout_logger("debug"), testController, config) - testRunner = new RspecTestRunner(stdout_logger("debug"), testController, config, testSuite, workspaceFolder) + testController = new StubTestController(logger) + testSuite = new TestSuite(logger, testController, config) + testRunner = new RspecTestRunner(logger, testController, config, testSuite, workspaceFolder) let mockProfile = mock() when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) - testLoader = new TestLoader(stdout_logger("debug"), testController, resolveTestsProfile, config, testSuite); + testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); }) test('Load tests on file resolve request', async function () { @@ -88,7 +91,7 @@ suite('Extension Test for RSpec', function() { testController.items.add(subfolderItem) subfolderItem.children.add(createTest("square/square_spec.rb", "square_spec.rb")) - // No tests in suite initially, only test files and folders + // No tests in suite initially, just test files and folders testItemCollectionMatches(testController.items, [ { @@ -192,14 +195,14 @@ suite('Extension Test for RSpec', function() { let cancellationTokenSource = new vscode.CancellationTokenSource(); before(async function() { - testController = new StubTestController(stdout_logger()) - testSuite = new TestSuite(stdout_logger(), testController, config) - testRunner = new RspecTestRunner(stdout_logger("trace"), testController, config, testSuite, workspaceFolder) + testController = new StubTestController(logger) + testSuite = new TestSuite(logger, testController, config) + testRunner = new RspecTestRunner(logger, testController, config, testSuite, workspaceFolder) let mockProfile = mock() when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) - testLoader = new TestLoader(stdout_logger(), testController, resolveTestsProfile, config, testSuite); + testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); await testLoader.discoverAllFilesInWorkspace() }) @@ -282,16 +285,11 @@ suite('Extension Test for RSpec', function() { } ] for(const {status, expectedTest, failureExpectation} of params) { - let mockTestRun: vscode.TestRun - - beforeEach(async function() { + test(`id: ${expectedTest.id}, status: ${status}`, async function() { let mockRequest = setupMockRequest(testSuite, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! - }) - - test(`id: ${expectedTest.id}, status: ${status}`, function() { + let mockTestRun = (testController as StubTestController).getMockTestRun(request)! switch(status) { case "passed": testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) From e61f57e44123e32bdf98d9a0237ada4a374d981a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 04:19:47 +0000 Subject: [PATCH 055/108] Tidy test logger and construction of TestRunner & TestLoader --- src/testFactory.ts | 6 -- src/testLoader.ts | 11 +-- src/testRunner.ts | 20 ++--- src/testSuite.ts | 4 +- test/stubs/logger.ts | 109 +++++++++++++++++++++++ test/stubs/noopLogger.ts | 18 ---- test/suite/helpers.ts | 111 ++---------------------- test/suite/minitest/minitest.test.ts | 23 ++--- test/suite/rspec/rspec.test.ts | 34 ++++---- test/suite/unitTests/config.test.ts | 10 ++- test/suite/unitTests/testRunner.test.ts | 19 ++-- test/suite/unitTests/testSuite.test.ts | 7 +- 12 files changed, 177 insertions(+), 195 deletions(-) create mode 100644 test/stubs/logger.ts delete mode 100644 test/stubs/noopLogger.ts diff --git a/src/testFactory.ts b/src/testFactory.ts index 8b52222..2a52adc 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -39,15 +39,11 @@ export class TestFactory implements vscode.Disposable { this.runner = this.framework == "rspec" ? new RspecTestRunner( this.log, - this.controller, - this.config as RspecConfig, this.testSuite, this.workspace, ) : new MinitestTestRunner( this.log, - this.controller, - this.config as MinitestConfig, this.testSuite, this.workspace, ) @@ -60,9 +56,7 @@ export class TestFactory implements vscode.Disposable { if (!this.loader) { this.loader = new TestLoader( this.log, - this.controller, this.profiles.resolveTestsProfile, - this.config, this.testSuite ) this.disposables.push(this.loader) diff --git a/src/testLoader.ts b/src/testLoader.ts index 48764d9..fd352ac 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import { IChildLogger } from '@vscode-logging/logger'; -import { Config } from './config'; import { TestSuite } from './testSuite'; export type ParsedTest = { @@ -33,9 +32,7 @@ export class TestLoader implements vscode.Disposable { constructor( readonly rootLog: IChildLogger, - private readonly controller: vscode.TestController, private readonly resolveTestProfile: vscode.TestRunProfile, - private readonly config: Config, private readonly testSuite: TestSuite, ) { this.log = rootLog.getChildLogger({ label: "TestLoader" }); @@ -94,11 +91,11 @@ export class TestLoader implements vscode.Disposable { */ public async discoverAllFilesInWorkspace(): Promise { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) - let testDir = this.config.getAbsoluteTestDirectory() + let testDir = this.testSuite.config.getAbsoluteTestDirectory() log.debug(`testDir: ${testDir}`) let patterns: Array = [] - this.config.getFilePattern().forEach(pattern => { + this.testSuite.config.getFilePattern().forEach(pattern => { patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) }) @@ -114,7 +111,7 @@ export class TestLoader implements vscode.Disposable { */ public async loadTests(testItems?: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.config.frameworkName()})...`); + log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.testSuite.config.frameworkName()})...`); try { let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) @@ -147,7 +144,7 @@ export class TestLoader implements vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer")) { - this.controller.items.replace([]) + this.testSuite.controller.items.replace([]) this.discoverAllFilesInWorkspace(); } }) diff --git a/src/testRunner.ts b/src/testRunner.ts index 339dac9..c944570 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -5,8 +5,6 @@ import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; -import { RspecConfig } from './rspec/rspecConfig'; -import { MinitestConfig } from './minitest/minitestConfig'; import { TestSuite } from './testSuite'; import { ParsedTest } from './testLoader'; @@ -25,8 +23,6 @@ export abstract class TestRunner implements vscode.Disposable { */ constructor( readonly rootLog: IChildLogger, - protected controller: vscode.TestController, - protected config: RspecConfig | MinitestConfig, protected testSuite: TestSuite, protected workspace?: vscode.WorkspaceFolder, ) { @@ -113,7 +109,7 @@ export abstract class TestRunner implements vscode.Disposable { */ async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; - let log = this.log.getChildLogger({ label: `ChildProcess(${this.config.frameworkName()})` }) + let log = this.log.getChildLogger({ label: `ChildProcess(${this.testSuite.config.frameworkName()})` }) process.stderr!.pipe(split2()).on('data', (data) => { data = data.toString(); @@ -221,7 +217,7 @@ export abstract class TestRunner implements vscode.Disposable { this.rootLog, token, request, - this.controller + this.testSuite.controller ); try { @@ -232,19 +228,19 @@ export abstract class TestRunner implements vscode.Disposable { let command: string if (context.request.profile?.label === 'ResolveTests') { - command = this.config.getResolveTestsCommand(testsToRun) + command = this.testSuite.config.getResolveTestsCommand(testsToRun) let testsRun = await this.runTestFramework(command, context) this.testSuite.removeMissingTests(testsRun, testsToRun) } else if (!testsToRun) { log.debug("Running all tests") - this.controller.items.forEach((testSuite) => { + this.testSuite.controller.items.forEach((item) => { // Mark selected tests as started - this.enqueTestAndChildren(testSuite, context) + this.enqueTestAndChildren(item, context) }) - command = this.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.testSuite.config.getFullTestSuiteCommand(context.debuggerConfig) } else { log.debug("Running selected tests") - command = this.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.testSuite.config.getFullTestSuiteCommand(context.debuggerConfig) for (const node of testsToRun) { log.trace("Adding test to command", node.id) // Mark selected tests as started @@ -422,7 +418,7 @@ export abstract class TestRunner implements vscode.Disposable { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, - env: this.config.getProcessEnv() + env: this.testSuite.config.getProcessEnv() }; this.log.debug(`Running command: ${testCommand}`); diff --git a/src/testSuite.ts b/src/testSuite.ts index c50e307..0ead76d 100644 --- a/src/testSuite.ts +++ b/src/testSuite.ts @@ -14,8 +14,8 @@ export class TestSuite { constructor( readonly rootLog: IChildLogger, - private readonly controller: vscode.TestController, - private readonly config: Config + public readonly controller: vscode.TestController, + public readonly config: Config ) { this.log = rootLog.getChildLogger({label: "TestSuite"}); } diff --git a/test/stubs/logger.ts b/test/stubs/logger.ts new file mode 100644 index 0000000..754ea8a --- /dev/null +++ b/test/stubs/logger.ts @@ -0,0 +1,109 @@ +import { LogLevel } from "@vscode-logging/logger"; +import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; + +function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogger { + let prependLabel = (l:string, m:string):string => `${l}: ${m}` + return { + ...parent, + debug: (msg: string, ...args: any[]) => { parent.debug(prependLabel(label, msg), ...args) }, + error: (msg: string, ...args: any[]) => { parent.error(prependLabel(label, msg), ...args) }, + fatal: (msg: string, ...args: any[]) => { parent.fatal(prependLabel(label, msg), ...args) }, + info: (msg: string, ...args: any[]) => { parent.info(prependLabel(label, msg), ...args) }, + trace: (msg: string, ...args: any[]) => { parent.trace(prependLabel(label, msg), ...args) }, + warn: (msg: string, ...args: any[]) => { parent.warn(prependLabel(label, msg), ...args) } + } +} + +function noop() {} + +/** + * Noop logger for use in testing where logs are usually unnecessary + */ +export const NOOP_LOGGER: IVSCodeExtLogger = { + changeLevel: noop, + changeSourceLocationTracking: noop, + debug: noop, + error: noop, + fatal: noop, + getChildLogger(opts: { label: string }): IChildLogger { + return this; + }, + info: noop, + trace: noop, + warn: noop +}; +Object.freeze(NOOP_LOGGER); + +function stdout_logger(level: LogLevel = "info"): IVSCodeExtLogger { + const levels: { [key: string]: number } = { + "fatal": 0, + "error": 1, + "warn": 2, + "info": 3, + "debug": 4, + "trace": 5, + } + const divider = '----------' + let maxLevel = levels[level] + function writeStdOutLogMsg(level: LogLevel, msg: string, ...args: any[]): void { + if (levels[level] <= maxLevel) { + let message = `[${level}] ${msg}${args.length > 0 ? ':' : ''}` + args.forEach((arg) => { + if (arg instanceof Error) { + message = `${message}\n ${arg.stack ? arg.stack : arg.name + ': ' + arg.message}` + } else { + message = `${message}\n ${JSON.stringify(arg)}` + } + }) + switch(level) { + case "fatal": + case "error": + console.error(message) + console.error(divider) + break; + case "warn": + console.warn(message) + console.warn(divider) + break; + case "info": + console.info(message) + console.info(divider) + break; + case "debug": + case "trace": + console.debug(message) + console.debug(divider) + break; + } + } + } + let logger: IVSCodeExtLogger = { + changeLevel: (level: LogLevel) => { maxLevel = levels[level] }, + changeSourceLocationTracking: noop, + debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, + error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, + fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, + getChildLogger(opts: { label: string }): IChildLogger { + return createChildLogger(this, opts.label); + }, + info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, + trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, + warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } + } + return logger +} + +/** + * Get a logger + * + * @param level One of "off", "fatal", "error", "warn", "info", "debug", "trace" + * @returns a noop logger if level is "off", else a logger that logs to stdout at the specified level and below + * (e.g. logger("warn") would return a logger that logs only messages logged at "fatal", "error", and "warn" levels) + */ +export function logger(level: LogLevel = "info"): IVSCodeExtLogger { + if (level == "off") { + return NOOP_LOGGER + } else { + return stdout_logger(level) + } +} diff --git a/test/stubs/noopLogger.ts b/test/stubs/noopLogger.ts deleted file mode 100644 index 56a8632..0000000 --- a/test/stubs/noopLogger.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; - -export function noop() {} - -export const NOOP_LOGGER: IVSCodeExtLogger = { - changeLevel: noop, - changeSourceLocationTracking: noop, - debug: noop, - error: noop, - fatal: noop, - getChildLogger(opts: { label: string }): IChildLogger { - return this; - }, - info: noop, - trace: noop, - warn: noop -}; -Object.freeze(NOOP_LOGGER); diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index c5afc3f..0a85d9e 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,113 +1,13 @@ import * as vscode from 'vscode' import { expect } from 'chai' -import { IVSCodeExtLogger, IChildLogger, LogLevel } from "@vscode-logging/types"; +import { IChildLogger } from "@vscode-logging/types"; import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; +import { NOOP_LOGGER } from '../stubs/logger'; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; import { TestSuite } from '../../src/testSuite'; -export function noop() {} - -/** - * Noop logger for use in testing where logs are usually unnecessary - */ -const NOOP_LOGGER: IVSCodeExtLogger = { - changeLevel: noop, - changeSourceLocationTracking: noop, - debug: noop, - error: noop, - fatal: noop, - getChildLogger(opts: { label: string }): IChildLogger { - return this; - }, - info: noop, - trace: noop, - warn: noop -} -Object.freeze(NOOP_LOGGER) - -function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogger { - let prependLabel = (l:string, m:string):string => `${l}: ${m}` - return { - ...parent, - debug: (msg: string, ...args: any[]) => { parent.debug(prependLabel(label, msg), ...args) }, - error: (msg: string, ...args: any[]) => { parent.error(prependLabel(label, msg), ...args) }, - fatal: (msg: string, ...args: any[]) => { parent.fatal(prependLabel(label, msg), ...args) }, - info: (msg: string, ...args: any[]) => { parent.info(prependLabel(label, msg), ...args) }, - trace: (msg: string, ...args: any[]) => { parent.trace(prependLabel(label, msg), ...args) }, - warn: (msg: string, ...args: any[]) => { parent.warn(prependLabel(label, msg), ...args) } - } -} - -/** - * Get a noop logger for use in testing where logs are usually unnecessary - */ -export function noop_logger(): IVSCodeExtLogger { return NOOP_LOGGER } - -/** - * Get a logger that logs to stdout. - * - * Not terribly pretty but useful for seeing what failing tests are doing - */ -export function stdout_logger(level: LogLevel = "info"): IVSCodeExtLogger { - const levels: { [key: string]: number } = { - "fatal": 0, - "error": 1, - "warn": 2, - "info": 3, - "debug": 4, - "trace": 5, - } - const divider = '----------' - let maxLevel = levels[level] - function writeStdOutLogMsg(level: LogLevel, msg: string, ...args: any[]): void { - if (levels[level] <= maxLevel) { - let message = `[${level}] ${msg}${args.length > 0 ? ':' : ''}` - args.forEach((arg) => { - if (arg instanceof Error) { - message = `${message}\n ${arg.stack ? arg.stack : arg.name + ': ' + arg.message}` - } else { - message = `${message}\n ${JSON.stringify(arg)}` - } - }) - switch(level) { - case "fatal": - case "error": - console.error(message) - console.error(divider) - break; - case "warn": - console.warn(message) - console.warn(divider) - break; - case "info": - console.info(message) - console.info(divider) - break; - case "debug": - case "trace": - console.debug(message) - console.debug(divider) - break; - } - } - } - let logger: IVSCodeExtLogger = { - changeLevel: (level: LogLevel) => { maxLevel = levels[level] }, - changeSourceLocationTracking: noop, - debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, - error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, - fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, - getChildLogger(opts: { label: string }): IChildLogger { - return createChildLogger(this, opts.label); - }, - info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, - trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, - warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } - } - return logger -} /** * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values @@ -237,12 +137,12 @@ export function setupMockTestController(rootLog?: IChildLogger): vscode.TestCont busy: false, range: undefined, error: undefined, - children: new StubTestItemCollection(rootLog || noop_logger(), instance(mockTestController)), + children: new StubTestItemCollection(rootLog || NOOP_LOGGER, instance(mockTestController)), } } when(mockTestController.createTestItem(anyString(), anyString())).thenCall(createTestItem) when(mockTestController.createTestItem(anyString(), anyString(), anything())).thenCall(createTestItem) - let testItems = new StubTestItemCollection(rootLog || noop_logger(), instance(mockTestController)) + let testItems = new StubTestItemCollection(rootLog || NOOP_LOGGER, instance(mockTestController)) when(mockTestController.items).thenReturn(testItems) return mockTestController } @@ -265,6 +165,9 @@ export function setupMockRequest(testSuite: TestSuite, testId?: string | string[ when(mockRequest.include).thenReturn(undefined) } when(mockRequest.exclude).thenReturn([]) + let mockRunProfile = mock() + when(mockRunProfile.label).thenReturn('Run') + when(mockRequest.profile).thenReturn(instance(mockRunProfile)) return mockRequest } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 7f18fac..85fb8e7 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -9,7 +9,8 @@ import { TestSuite } from '../../../src/testSuite'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { stdout_logger, setupMockRequest, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; +import { setupMockRequest, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; +import { logger } from '../..//stubs/logger'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { @@ -21,8 +22,8 @@ suite('Extension Test for Minitest', function() { let testSuite: TestSuite; let resolveTestsProfile: vscode.TestRunProfile; - //const logger = noop_logger(); - const logger = stdout_logger("info"); + //const logger = NOOP_LOGGER; + const log = logger("info"); let expectedPath = (file: string): string => { return path.resolve( @@ -76,10 +77,10 @@ suite('Extension Test for Minitest', function() { suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(logger) - testSuite = new TestSuite(logger, testController, config) - testRunner = new MinitestTestRunner(logger, testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, config) + testRunner = new MinitestTestRunner(log, testSuite, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, testSuite); }) test('Load tests on file resolve request', async function () { @@ -203,10 +204,10 @@ suite('Extension Test for Minitest', function() { let cancellationTokenSource = new vscode.CancellationTokenSource(); before(async function() { - testController = new StubTestController(logger) - testSuite = new TestSuite(logger, testController, config) - testRunner = new MinitestTestRunner(logger, testController, config, testSuite, workspaceFolder) - testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, config) + testRunner = new MinitestTestRunner(log, testSuite, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, testSuite); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 91fff42..997058b 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -9,7 +9,8 @@ import { TestSuite } from '../../../src/testSuite'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { stdout_logger, setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; +import { setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; +import { logger } from '../../stubs/logger'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { @@ -21,8 +22,7 @@ suite('Extension Test for RSpec', function() { let testSuite: TestSuite; let resolveTestsProfile: vscode.TestRunProfile; - //const logger = noop_logger(); - const logger = stdout_logger("info"); + const log = logger("info"); let expectedPath = (file: string): string => { return path.resolve( @@ -68,18 +68,18 @@ suite('Extension Test for RSpec', function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) config = new RspecConfig(path.resolve("ruby"), workspaceFolder) + let mockProfile = mock() + when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) + when(mockProfile.label).thenReturn('ResolveTests') + resolveTestsProfile = instance(mockProfile) }) suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(logger) - testSuite = new TestSuite(logger, testController, config) - testRunner = new RspecTestRunner(logger, testController, config, testSuite, workspaceFolder) - let mockProfile = mock() - when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) - when(mockProfile.label).thenReturn('ResolveTests') - resolveTestsProfile = instance(mockProfile) - testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, config) + testRunner = new RspecTestRunner(log, testSuite, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, testSuite); }) test('Load tests on file resolve request', async function () { @@ -195,14 +195,10 @@ suite('Extension Test for RSpec', function() { let cancellationTokenSource = new vscode.CancellationTokenSource(); before(async function() { - testController = new StubTestController(logger) - testSuite = new TestSuite(logger, testController, config) - testRunner = new RspecTestRunner(logger, testController, config, testSuite, workspaceFolder) - let mockProfile = mock() - when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) - when(mockProfile.label).thenReturn('ResolveTests') - resolveTestsProfile = instance(mockProfile) - testLoader = new TestLoader(logger, testController, resolveTestsProfile, config, testSuite); + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, config) + testRunner = new RspecTestRunner(log, testSuite, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, testSuite); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index a5e3a08..af0cddf 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -5,7 +5,9 @@ import * as path from 'path' import { Config } from "../../../src/config"; import { RspecConfig } from "../../../src/rspec/rspecConfig"; -import { noop_logger } from "../helpers"; +import { logger } from "../../stubs/logger"; + +const log = logger("off") suite('Config', function() { let setConfig = (testFramework: string) => { @@ -19,21 +21,21 @@ suite('Config', function() { let testFramework = "rspec" setConfig(testFramework) - expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + expect(Config.getTestFramework(log)).to.eq(testFramework); }); test('should return minitest when configuration set to minitest', function() { let testFramework = 'minitest' setConfig(testFramework) - expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + expect(Config.getTestFramework(log)).to.eq(testFramework); }); test('should return none when configuration set to none', function() { let testFramework = 'none' setConfig(testFramework) - expect(Config.getTestFramework(noop_logger())).to.eq(testFramework); + expect(Config.getTestFramework(log)).to.eq(testFramework); }); }); diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts index a95d08f..94ae9c0 100644 --- a/test/suite/unitTests/testRunner.test.ts +++ b/test/suite/unitTests/testRunner.test.ts @@ -7,13 +7,14 @@ import * as path from 'path' import { Config } from "../../../src/config"; import { TestSuite } from "../../../src/testSuite"; import { TestRunner } from "../../../src/testRunner"; -import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; -import { MinitestConfig } from '../../../src/minitest/minitestConfig'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; -import { noop_logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; +import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; +import { logger } from '../../stubs/logger'; import { StubTestController } from '../../stubs/stubTestController'; +const log = logger("off") + suite('TestRunner', function () { let testSuite: TestSuite let testController: vscode.TestController @@ -30,9 +31,9 @@ suite('TestRunner', function () { }) beforeEach(function () { - testController = new StubTestController(noop_logger()) - testSuite = new TestSuite(noop_logger(), testController, instance(config)) - testRunner = new RspecTestRunner(noop_logger(), testController, instance(config) as RspecConfig, testSuite) + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, instance(config)) + testRunner = new RspecTestRunner(log, testSuite) }) const expectedTests: TestItemExpectation[] = [ @@ -119,9 +120,9 @@ suite('TestRunner', function () { }) beforeEach(function () { - testController = new StubTestController(noop_logger()) - testSuite = new TestSuite(noop_logger(), testController, instance(config)) - testRunner = new MinitestTestRunner(noop_logger(), testController, instance(config) as MinitestConfig, testSuite) + testController = new StubTestController(log) + testSuite = new TestSuite(log, testController, instance(config)) + testRunner = new MinitestTestRunner(log, testSuite) }) const expectedTests: TestItemExpectation[] = [ diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index c7e01c0..9b8bc08 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -7,7 +7,8 @@ import path from 'path' import { Config } from '../../../src/config'; import { TestSuite } from '../../../src/testSuite'; import { StubTestController } from '../../stubs/stubTestController'; -import { noop_logger, testUriMatches } from '../helpers'; +import { NOOP_LOGGER } from '../../stubs/logger'; +import { testUriMatches } from '../helpers'; suite('TestSuite', function () { let mockConfig: Config = mock(); @@ -22,8 +23,8 @@ suite('TestSuite', function () { }); beforeEach(function () { - controller = new StubTestController(noop_logger()) - testSuite = new TestSuite(noop_logger(), controller, instance(mockConfig)) + controller = new StubTestController(NOOP_LOGGER) + testSuite = new TestSuite(NOOP_LOGGER, controller, instance(mockConfig)) }); suite('#normaliseTestId()', function () { From ba218d1d1d3795bac12502206afbedf19669dd8b Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 10:20:13 +0000 Subject: [PATCH 056/108] Rename TestSuite to TestSuiteManager --- src/minitest/minitestTestRunner.ts | 2 +- src/rspec/rspecTestRunner.ts | 2 +- src/testFactory.ts | 12 +++---- src/testLoader.ts | 20 +++++------ src/testRunner.ts | 34 +++++++++--------- src/{testSuite.ts => testSuiteManager.ts} | 42 +++++++++++------------ test/suite/helpers.ts | 8 ++--- test/suite/minitest/minitest.test.ts | 28 +++++++-------- test/suite/rspec/rspec.test.ts | 28 +++++++-------- test/suite/unitTests/testRunner.test.ts | 12 +++---- test/suite/unitTests/testSuite.test.ts | 40 ++++++++++----------- 11 files changed, 113 insertions(+), 115 deletions(-) rename src/{testSuite.ts => testSuiteManager.ts} (88%) diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 2c541ec..86d9618 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -13,7 +13,7 @@ export class MinitestTestRunner extends TestRunner { */ handleStatus(test: any, context: TestRunContext): void { this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); - let testItem = this.testSuite.getOrCreateTestItem(test.id) + let testItem = this.manager.getOrCreateTestItem(test.id) if (test.status === "passed") { context.passed(testItem) } else if (test.status === "failed" && test.pending_message === null) { diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 7cf8303..1113653 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -15,7 +15,7 @@ export class RspecTestRunner extends TestRunner { handleStatus(test: ParsedTest, context: TestRunContext): void { let log = this.log.getChildLogger({ label: "handleStatus" }) log.trace(`Handling status of test: ${JSON.stringify(test)}`); - let testItem = this.testSuite.getOrCreateTestItem(test.id) + let testItem = this.manager.getOrCreateTestItem(test.id) if (test.status === "passed") { log.trace("Passed", testItem.id) context.passed(testItem) diff --git a/src/testFactory.ts b/src/testFactory.ts index 2a52adc..81a2afc 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -6,14 +6,14 @@ import { Config } from './config'; import { TestLoader } from './testLoader'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; -import { TestSuite } from './testSuite'; +import { TestSuiteManager } from './testSuiteManager'; export class TestFactory implements vscode.Disposable { private loader: TestLoader | null = null; private runner: RspecTestRunner | MinitestTestRunner | null = null; protected disposables: { dispose(): void }[] = []; protected framework: string; - private testSuite: TestSuite + private manager: TestSuiteManager constructor( private readonly log: IVSCodeExtLogger, @@ -24,7 +24,7 @@ export class TestFactory implements vscode.Disposable { ) { this.disposables.push(this.configWatcher()); this.framework = Config.getTestFramework(this.log); - this.testSuite = new TestSuite(this.log, this.controller, this.config) + this.manager = new TestSuiteManager(this.log, this.controller, this.config) } dispose(): void { @@ -39,12 +39,12 @@ export class TestFactory implements vscode.Disposable { this.runner = this.framework == "rspec" ? new RspecTestRunner( this.log, - this.testSuite, + this.manager, this.workspace, ) : new MinitestTestRunner( this.log, - this.testSuite, + this.manager, this.workspace, ) this.disposables.push(this.runner); @@ -57,7 +57,7 @@ export class TestFactory implements vscode.Disposable { this.loader = new TestLoader( this.log, this.profiles.resolveTestsProfile, - this.testSuite + this.manager ) this.disposables.push(this.loader) } diff --git a/src/testLoader.ts b/src/testLoader.ts index fd352ac..a8645e8 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { IChildLogger } from '@vscode-logging/logger'; -import { TestSuite } from './testSuite'; +import { TestSuiteManager } from './testSuiteManager'; export type ParsedTest = { id: string, @@ -33,7 +33,7 @@ export class TestLoader implements vscode.Disposable { constructor( readonly rootLog: IChildLogger, private readonly resolveTestProfile: vscode.TestRunProfile, - private readonly testSuite: TestSuite, + private readonly manager: TestSuiteManager, ) { this.log = rootLog.getChildLogger({ label: "TestLoader" }); this.disposables.push(this.configWatcher()); @@ -59,7 +59,7 @@ export class TestLoader implements vscode.Disposable { // When files are created, make sure there's a corresponding "file" node in the tree watcher.onDidCreate(uri => { log.debug(`onDidCreate ${uri.fsPath}`) - this.testSuite.getOrCreateTestItem(uri) + this.manager.getOrCreateTestItem(uri) }) // When files change, re-parse them. Note that you could optimize this so // that you only re-parse children that have been resolved in the past. @@ -71,12 +71,12 @@ export class TestLoader implements vscode.Disposable { // we use the URI as the TestItem's ID. watcher.onDidDelete(uri => { log.debug(`onDidDelete ${uri.fsPath}`) - this.testSuite.deleteTestItem(uri) + this.manager.deleteTestItem(uri) }); for (const file of await vscode.workspace.findFiles(pattern)) { log.debug("Found file, creating TestItem", file) - this.testSuite.getOrCreateTestItem(file) + this.manager.getOrCreateTestItem(file) } log.debug("Resolving tests in found files") @@ -91,11 +91,11 @@ export class TestLoader implements vscode.Disposable { */ public async discoverAllFilesInWorkspace(): Promise { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) - let testDir = this.testSuite.config.getAbsoluteTestDirectory() + let testDir = this.manager.config.getAbsoluteTestDirectory() log.debug(`testDir: ${testDir}`) let patterns: Array = [] - this.testSuite.config.getFilePattern().forEach(pattern => { + this.manager.config.getFilePattern().forEach(pattern => { patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) }) @@ -111,7 +111,7 @@ export class TestLoader implements vscode.Disposable { */ public async loadTests(testItems?: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.testSuite.config.frameworkName()})...`); + log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.manager.config.frameworkName()})...`); try { let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) @@ -125,7 +125,7 @@ export class TestLoader implements vscode.Disposable { let log = this.log.getChildLogger({label: "parseTestsInFile"}) let testItem: vscode.TestItem if ("fsPath" in uri) { - let test = this.testSuite.getTestItem(uri) + let test = this.manager.getTestItem(uri) if (!test) { return } @@ -144,7 +144,7 @@ export class TestLoader implements vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer")) { - this.testSuite.controller.items.replace([]) + this.manager.controller.items.replace([]) this.discoverAllFilesInWorkspace(); } }) diff --git a/src/testRunner.ts b/src/testRunner.ts index c944570..4e81f70 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -5,7 +5,7 @@ import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; -import { TestSuite } from './testSuite'; +import { TestSuiteManager } from './testSuiteManager'; import { ParsedTest } from './testLoader'; export abstract class TestRunner implements vscode.Disposable { @@ -17,13 +17,11 @@ export abstract class TestRunner implements vscode.Disposable { /** * @param rootLog The Test Adapter logger, for logging. * @param workspace Open workspace folder - * @param controller Test controller that holds the test suite - * @param config Configuration provider - * @param testSuite TestSuite instance + * @param manager TestSuiteManager instance */ constructor( readonly rootLog: IChildLogger, - protected testSuite: TestSuite, + protected manager: TestSuiteManager, protected workspace?: vscode.WorkspaceFolder, ) { this.log = rootLog.getChildLogger({label: "TestRunner"}) @@ -109,7 +107,7 @@ export abstract class TestRunner implements vscode.Disposable { */ async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { this.currentChildProcess = process; - let log = this.log.getChildLogger({ label: `ChildProcess(${this.testSuite.config.frameworkName()})` }) + let log = this.log.getChildLogger({ label: `ChildProcess(${this.manager.config.frameworkName()})` }) process.stderr!.pipe(split2()).on('data', (data) => { data = data.toString(); @@ -122,8 +120,8 @@ export abstract class TestRunner implements vscode.Disposable { let parsedTests: vscode.TestItem[] = [] process.stdout!.pipe(split2()).on('data', (data) => { let getTest = (testId: string): vscode.TestItem => { - testId = this.testSuite.normaliseTestId(testId) - return this.testSuite.getOrCreateTestItem(testId) + testId = this.manager.normaliseTestId(testId) + return this.manager.getOrCreateTestItem(testId) } if (data.startsWith('PASSED:')) { log.debug(`Received test status - PASSED`, data) @@ -217,7 +215,7 @@ export abstract class TestRunner implements vscode.Disposable { this.rootLog, token, request, - this.testSuite.controller + this.manager.controller ); try { @@ -228,19 +226,19 @@ export abstract class TestRunner implements vscode.Disposable { let command: string if (context.request.profile?.label === 'ResolveTests') { - command = this.testSuite.config.getResolveTestsCommand(testsToRun) + command = this.manager.config.getResolveTestsCommand(testsToRun) let testsRun = await this.runTestFramework(command, context) - this.testSuite.removeMissingTests(testsRun, testsToRun) + this.manager.removeMissingTests(testsRun, testsToRun) } else if (!testsToRun) { log.debug("Running all tests") - this.testSuite.controller.items.forEach((item) => { + this.manager.controller.items.forEach((item) => { // Mark selected tests as started this.enqueTestAndChildren(item, context) }) - command = this.testSuite.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) } else { log.debug("Running selected tests") - command = this.testSuite.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) for (const node of testsToRun) { log.trace("Adding test to command", node.id) // Mark selected tests as started @@ -319,7 +317,7 @@ export abstract class TestRunner implements vscode.Disposable { let parsedTests: vscode.TestItem[] = [] if (tests && tests.length > 0) { tests.forEach((test: ParsedTest) => { - test.id = this.testSuite.normaliseTestId(test.id) + test.id = this.manager.normaliseTestId(test.id) // RSpec provides test ids like "file_name.rb[1:2:3]". // This uses the digits at the end of the id to create @@ -327,7 +325,7 @@ export abstract class TestRunner implements vscode.Disposable { // test in the file. let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); let testNumber = test_location_array[test_location_array.length - 1]; - test.file_path = this.testSuite.normaliseTestId(test.file_path).replace(/\[.*/, '') + test.file_path = this.manager.normaliseTestId(test.file_path).replace(/\[.*/, '') let currentFileLabel = test.file_path.split(path.sep).slice(-1)[0] let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" @@ -348,7 +346,7 @@ export abstract class TestRunner implements vscode.Disposable { let test_location_string: string = test_location_array.join(''); test.location = parseInt(test_location_string); - let newTestItem = this.testSuite.getOrCreateTestItem(test.id) + let newTestItem = this.manager.getOrCreateTestItem(test.id) newTestItem.canResolveChildren = !test.id.endsWith(']') log.trace(`canResolveChildren (${test.id}): ${newTestItem.canResolveChildren}`) log.trace(`Setting test ${newTestItem.id} label to "${description}"`) @@ -418,7 +416,7 @@ export abstract class TestRunner implements vscode.Disposable { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, - env: this.testSuite.config.getProcessEnv() + env: this.manager.config.getProcessEnv() }; this.log.debug(`Running command: ${testCommand}`); diff --git a/src/testSuite.ts b/src/testSuiteManager.ts similarity index 88% rename from src/testSuite.ts rename to src/testSuiteManager.ts index 0ead76d..93dd555 100644 --- a/src/testSuite.ts +++ b/src/testSuiteManager.ts @@ -4,11 +4,11 @@ import { IChildLogger } from '@vscode-logging/logger'; import { Config } from './config'; /** - * Manages the contents of the test suite + * Manages the contents and state of the test suite * * Responsible for creating, deleting and finding test items */ -export class TestSuite { +export class TestSuiteManager { private readonly log: IChildLogger; private readonly locationPattern = /\[[0-9]*(?::[0-9]*)*\]$/ @@ -17,27 +17,27 @@ export class TestSuite { public readonly controller: vscode.TestController, public readonly config: Config ) { - this.log = rootLog.getChildLogger({label: "TestSuite"}); + this.log = rootLog.getChildLogger({label: 'TestSuite'}); } public deleteTestItem(testId: string | vscode.Uri) { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.uriToTestId(testId) - log.debug(`Deleting test ${testId}`) + log.debug('Deleting test', testId) let parent = this.getOrCreateParent(testId, false) let collection: vscode.TestItemCollection | undefined if (!parent) { log.debug('Parent is controller') collection = this.controller.items } else { - log.debug(`Parent is ${parent.id}`) + log.debug('Parent', parent.id) collection = parent.children } if (collection) { collection.delete(testId); - log.debug(`Removed test ${testId}`) + log.debug('Removed test', testId) } else { - log.error("Collection not found") + log.error('Collection not found') } } @@ -53,7 +53,7 @@ export class TestSuite { if (testId.startsWith(`.${path.sep}`)) { testId = testId.substring(2) } - log.debug(`Looking for test ${testId}`) + log.debug('Looking for test', testId) let parent = this.getOrCreateParent(testId, true) let testItem = (parent?.children || this.controller.items).get(testId) if (!testItem) { @@ -83,7 +83,7 @@ export class TestSuite { let parent = this.getOrCreateParent(testId, false) let testItem = (parent?.children || this.controller.items).get(testId) if (!testItem) { - log.debug(`Couldn't find ${testId}`) + log.debug("Couldn't find test with ID", testId) return undefined } return testItem @@ -92,7 +92,7 @@ export class TestSuite { public removeMissingTests(parsedTests: vscode.TestItem[], requestedTests?: readonly vscode.TestItem[]) { let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) - log.debug("Tests to check", JSON.stringify(parsedTests.map(x => x.id))) + log.debug('Tests to check', JSON.stringify(parsedTests.map(x => x.id))) // Files and folders are removed by the filesystem watchers so we only need to clear out single tests parsedTests = parsedTests.filter(x => !x.canResolveChildren && x.parent) @@ -101,14 +101,14 @@ export class TestSuite { if (!requestedTests || requestedTests.includes(parent!)) { // If full suite was resolved we can always replace. If partial suite was resolved, we should // only replace children if parent was resolved, else we might remove tests that do exist - log.debug("Checking parent", parent?.id) + log.debug('Checking parent', parent?.id) let parentCollection = parent ? parent.children : this.controller.items let parentCollectionSize = parentCollection.size parentCollection.replace(parsedTests.filter(x => x.parent == parent)) - log.debug("Removed tests from parent", parentCollectionSize - parentCollection.size) + log.debug('Removed tests from parent', parentCollectionSize - parentCollection.size) } parsedTests = parsedTests.filter(x => x.parent != parent) - log.debug("Remaining tests to check", parsedTests.length) + log.debug('Remaining tests to check', parsedTests.length) } } @@ -129,7 +129,7 @@ export class TestSuite { if (testId.startsWith(path.sep)) { testId = testId.substring(1) } - log.debug(`Normalised ID: ${testId}`) + log.debug('Normalised ID', testId) return testId } @@ -145,9 +145,9 @@ export class TestSuite { return uri } let fullTestDirPath = this.config.getAbsoluteTestDirectory() - log.debug(`Full path to test dir: ${fullTestDirPath}`) + log.debug('Full path to test dir', fullTestDirPath) let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') - log.debug(`Stripped URI: ${strippedUri}`) + log.debug('Stripped URI', strippedUri) return strippedUri } @@ -169,7 +169,7 @@ export class TestSuite { // Walk through test folders to find the collection containing our test file for (let i = 0; i < idSegments.length - 1; i++) { let collectionId = this.getPartialId(idSegments, i) - log.debug(`Getting parent collection ${collectionId}`) + log.debug('Getting parent collection', collectionId) let child = this.controller.items.get(collectionId) if (!child) { if (!createIfMissing) return undefined @@ -191,7 +191,7 @@ export class TestSuite { } let child = (parent?.children || this.controller.items).get(fileId) if (!child) { - log.debug(`TestItem for file ${fileId} not in parent collection`) + log.debug('TestItem for file not in parent collection', fileId) if (!createIfMissing) return undefined child = this.createTestItem( fileId, @@ -199,7 +199,7 @@ export class TestSuite { parent, ) } - log.debug(`Got TestItem for file ${fileId} from parent collection`) + log.debug('Got TestItem for file from parent collection', fileId) parent = child } // else test item is the file so return the file's parent @@ -223,11 +223,11 @@ export class TestSuite { ): vscode.TestItem { let log = this.log.getChildLogger({ label: `${this.createTestItem.name}(${testId})` }) let uri = this.testIdToUri(testId) - log.debug(`Creating test item - label: ${label}, parent: ${parent?.id}, canResolveChildren: ${canResolveChildren}, uri: ${uri},`) + log.debug('Creating test item', {label: label, parentId: parent?.id, canResolveChildren: canResolveChildren, uri: uri}) let item = this.controller.createTestItem(testId, label, uri) item.canResolveChildren = canResolveChildren; (parent?.children || this.controller.items).add(item); - log.debug(`Added test ${item.id}`) + log.debug('Added test', item.id) return item } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 0a85d9e..6f9f0e3 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -6,7 +6,7 @@ import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCa import { NOOP_LOGGER } from '../stubs/logger'; import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; -import { TestSuite } from '../../src/testSuite'; +import { TestSuiteManager } from '../testSuiteManager'; /** @@ -147,18 +147,18 @@ export function setupMockTestController(rootLog?: IChildLogger): vscode.TestCont return mockTestController } -export function setupMockRequest(testSuite: TestSuite, testId?: string | string[]): vscode.TestRunRequest { +export function setupMockRequest(manager: TestSuiteManager, testId?: string | string[]): vscode.TestRunRequest { let mockRequest = mock() if (testId) { if (Array.isArray(testId)) { let testItems: vscode.TestItem[] = [] testId.forEach(id => { - let testItem = testSuite.getOrCreateTestItem(id) + let testItem = manager.getOrCreateTestItem(id) testItems.push(testItem) }) when(mockRequest.include).thenReturn(testItems) } else { - let testItem = testSuite.getOrCreateTestItem(testId as string) + let testItem = manager.getOrCreateTestItem(testId as string) when(mockRequest.include).thenReturn([testItem]) } } else { diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 85fb8e7..00dfa81 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { before, beforeEach } from 'mocha'; import { TestLoader } from '../../../src/testLoader'; -import { TestSuite } from '../../../src/testSuite'; +import { TestSuiteManager } from '../../testSuiteManager'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; @@ -19,7 +19,7 @@ suite('Extension Test for Minitest', function() { let config: MinitestConfig let testRunner: MinitestTestRunner; let testLoader: TestLoader; - let testSuite: TestSuite; + let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; //const logger = NOOP_LOGGER; @@ -78,9 +78,9 @@ suite('Extension Test for Minitest', function() { suite('dry run', function() { beforeEach(function () { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, config) - testRunner = new MinitestTestRunner(log, testSuite, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, testSuite); + manager = new TestSuiteManager(log, testController, config) + testRunner = new MinitestTestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); }) test('Load tests on file resolve request', async function () { @@ -162,9 +162,9 @@ suite('Extension Test for Minitest', function() { test('Load all tests', async function () { await testLoader.discoverAllFilesInWorkspace() - const testSuite = testController.items + const manager = testController.items - testItemCollectionMatches(testSuite, + testItemCollectionMatches(manager, [ { file: expectedPath("abs_test.rb"), @@ -205,9 +205,9 @@ suite('Extension Test for Minitest', function() { before(async function() { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, config) - testRunner = new MinitestTestRunner(log, testSuite, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, testSuite); + manager = new TestSuiteManager(log, testController, config) + testRunner = new MinitestTestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) @@ -215,7 +215,7 @@ suite('Extension Test for Minitest', function() { let mockTestRun: vscode.TestRun test('when running full suite', async function() { - let mockRequest = setupMockRequest(testSuite) + let mockRequest = setupMockRequest(manager) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -229,7 +229,7 @@ suite('Extension Test for Minitest', function() { }) test('when running all top-level items', async function() { - let mockRequest = setupMockRequest(testSuite, ["abs_test.rb", "square"]) + let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -243,7 +243,7 @@ suite('Extension Test for Minitest', function() { }) test('when running all files', async function() { - let mockRequest = setupMockRequest(testSuite, ["abs_test.rb", "square/square_test.rb"]) + let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square/square_test.rb"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -291,7 +291,7 @@ suite('Extension Test for Minitest', function() { ] for(const {status, expectedTest, failureExpectation} of params) { test(`id: ${expectedTest.id}, status: ${status}`, async function() { - let mockRequest = setupMockRequest(testSuite, expectedTest.id) + let mockRequest = setupMockRequest(manager, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) let mockTestRun = (testController as StubTestController).getMockTestRun(request)! diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 997058b..ef8099e 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -5,7 +5,7 @@ import { before, beforeEach } from 'mocha'; import { expect } from 'chai'; import { TestLoader } from '../../../src/testLoader'; -import { TestSuite } from '../../../src/testSuite'; +import { TestSuiteManager } from '../../testSuiteManager'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; @@ -19,7 +19,7 @@ suite('Extension Test for RSpec', function() { let config: RspecConfig let testRunner: RspecTestRunner; let testLoader: TestLoader; - let testSuite: TestSuite; + let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; const log = logger("info"); @@ -77,9 +77,9 @@ suite('Extension Test for RSpec', function() { suite('dry run', function() { beforeEach(function () { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, config) - testRunner = new RspecTestRunner(log, testSuite, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, testSuite); + manager = new TestSuiteManager(log, testController, config) + testRunner = new RspecTestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); }) test('Load tests on file resolve request', async function () { @@ -153,9 +153,9 @@ suite('Extension Test for RSpec', function() { console.log("resolving files in test") await testLoader.discoverAllFilesInWorkspace() - const testSuite = testController.items + const manager = testController.items - testItemCollectionMatches(testSuite, + testItemCollectionMatches(manager, [ { file: expectedPath("abs_spec.rb"), @@ -196,9 +196,9 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, config) - testRunner = new RspecTestRunner(log, testSuite, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, testSuite); + manager = new TestSuiteManager(log, testController, config) + testRunner = new RspecTestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) @@ -206,7 +206,7 @@ suite('Extension Test for RSpec', function() { let mockTestRun: vscode.TestRun test('when running full suite', async function() { - let mockRequest = setupMockRequest(testSuite) + let mockRequest = setupMockRequest(manager) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -220,7 +220,7 @@ suite('Extension Test for RSpec', function() { }) test('when running all top-level items', async function() { - let mockRequest = setupMockRequest(testSuite, ["abs_spec.rb", "square"]) + let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -234,7 +234,7 @@ suite('Extension Test for RSpec', function() { }) test('when running all files', async function() { - let mockRequest = setupMockRequest(testSuite, ["abs_spec.rb", "square/square_spec.rb"]) + let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square/square_spec.rb"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! @@ -282,7 +282,7 @@ suite('Extension Test for RSpec', function() { ] for(const {status, expectedTest, failureExpectation} of params) { test(`id: ${expectedTest.id}, status: ${status}`, async function() { - let mockRequest = setupMockRequest(testSuite, expectedTest.id) + let mockRequest = setupMockRequest(manager, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) let mockTestRun = (testController as StubTestController).getMockTestRun(request)! diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts index 94ae9c0..4219c92 100644 --- a/test/suite/unitTests/testRunner.test.ts +++ b/test/suite/unitTests/testRunner.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import * as path from 'path' import { Config } from "../../../src/config"; -import { TestSuite } from "../../../src/testSuite"; +import { TestSuiteManager } from "../../testSuiteManager"; import { TestRunner } from "../../../src/testRunner"; import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; @@ -16,7 +16,7 @@ import { StubTestController } from '../../stubs/stubTestController'; const log = logger("off") suite('TestRunner', function () { - let testSuite: TestSuite + let manager: TestSuiteManager let testController: vscode.TestController let testRunner: TestRunner @@ -32,8 +32,8 @@ suite('TestRunner', function () { beforeEach(function () { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, instance(config)) - testRunner = new RspecTestRunner(log, testSuite) + manager = new TestSuiteManager(log, testController, instance(config)) + testRunner = new RspecTestRunner(log, manager) }) const expectedTests: TestItemExpectation[] = [ @@ -121,8 +121,8 @@ suite('TestRunner', function () { beforeEach(function () { testController = new StubTestController(log) - testSuite = new TestSuite(log, testController, instance(config)) - testRunner = new MinitestTestRunner(log, testSuite) + manager = new TestSuiteManager(log, testController, instance(config)) + testRunner = new MinitestTestRunner(log, manager) }) const expectedTests: TestItemExpectation[] = [ diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuite.test.ts index 9b8bc08..36aa155 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuite.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import path from 'path' import { Config } from '../../../src/config'; -import { TestSuite } from '../../../src/testSuite'; +import { TestSuiteManager } from '../../testSuiteManager'; import { StubTestController } from '../../stubs/stubTestController'; import { NOOP_LOGGER } from '../../stubs/logger'; import { testUriMatches } from '../helpers'; @@ -14,7 +14,7 @@ suite('TestSuite', function () { let mockConfig: Config = mock(); const config: Config = instance(mockConfig) let controller: vscode.TestController; - let testSuite: TestSuite; + let manager: TestSuiteManager; before(function () { let relativeTestPath = 'path/to/spec' @@ -24,7 +24,7 @@ suite('TestSuite', function () { beforeEach(function () { controller = new StubTestController(NOOP_LOGGER) - testSuite = new TestSuite(NOOP_LOGGER, controller, instance(mockConfig)) + manager = new TestSuiteManager(NOOP_LOGGER, controller, instance(mockConfig)) }); suite('#normaliseTestId()', function () { @@ -43,7 +43,7 @@ suite('TestSuite', function () { parameters.forEach(({ arg, expected }) => { test(`correctly normalises ${arg} to ${expected}`, function () { - expect(testSuite.normaliseTestId(arg)).to.eq(expected); + expect(manager.normaliseTestId(arg)).to.eq(expected); }); }); }); @@ -61,7 +61,7 @@ suite('TestSuite', function () { controller.items.add(secondTestItem) expect(controller.items.size).to.eq(2) - testSuite.deleteTestItem(id) + manager.deleteTestItem(id) expect(controller.items.size).to.eq(1) expect(controller.items.get('test-id-2')).to.eq(secondTestItem) @@ -70,7 +70,7 @@ suite('TestSuite', function () { test('does nothing if ID not found', function () { expect(controller.items.size).to.eq(1) - testSuite.deleteTestItem('test-id-2') + manager.deleteTestItem('test-id-2') expect(controller.items.size).to.eq(1) }) @@ -97,23 +97,23 @@ suite('TestSuite', function () { }) test('gets the specified test if ID is found', function () { - expect(testSuite.getTestItem(id)).to.eq(testItem) + expect(manager.getTestItem(id)).to.eq(testItem) }) test('returns undefined if ID is not found', function () { - expect(testSuite.getTestItem('not-found')).to.be.undefined + expect(manager.getTestItem('not-found')).to.be.undefined }) test('gets the specified nested test if ID is found', function () { - expect(testSuite.getTestItem(childId)).to.eq(childItem) + expect(manager.getTestItem(childId)).to.eq(childItem) }) test('returns undefined if nested ID is not found', function () { - expect(testSuite.getTestItem('folder/not-found')).to.be.undefined + expect(manager.getTestItem('folder/not-found')).to.be.undefined }) test('returns undefined if parent of nested ID is not found', function () { - expect(testSuite.getTestItem('not-found/child-test')).to.be.undefined + expect(manager.getTestItem('not-found/child-test')).to.be.undefined }) }) @@ -131,11 +131,11 @@ suite('TestSuite', function () { test('gets the specified item if ID is found', function () { controller.items.add(testItem) - expect(testSuite.getOrCreateTestItem(id)).to.eq(testItem) + expect(manager.getOrCreateTestItem(id)).to.eq(testItem) }) test('creates item if ID is not found', function () { - let testItem = testSuite.getOrCreateTestItem('not-found') + let testItem = manager.getOrCreateTestItem('not-found') expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq('not-found') testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), 'not-found')) @@ -147,7 +147,7 @@ suite('TestSuite', function () { folderItem.children.add(childItem) controller.items.add(folderItem) - expect(testSuite.getOrCreateTestItem(childId)).to.eq(childItem) + expect(manager.getOrCreateTestItem(childId)).to.eq(childItem) }) test('creates item if nested ID is not found', function () { @@ -155,7 +155,7 @@ suite('TestSuite', function () { let folderItem = controller.createTestItem('folder', 'folder') controller.items.add(folderItem) - let testItem = testSuite.getOrCreateTestItem(id) + let testItem = manager.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq(id) testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) @@ -163,11 +163,11 @@ suite('TestSuite', function () { test('creates item and parent if parent of nested file is not found', function () { let id = `folder${path.sep}not-found` - let testItem = testSuite.getOrCreateTestItem(id) + let testItem = manager.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq(id) - let folder = testSuite.getOrCreateTestItem('folder') + let folder = manager.getOrCreateTestItem('folder') expect(folder?.children.size).to.eq(1) expect(folder?.children.get(id)).to.eq(testItem) testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) @@ -182,12 +182,12 @@ suite('TestSuite', function () { ]) { test(suite, function() { let id = `${fileId}${location}` - let testItem = testSuite.getOrCreateTestItem(id) + let testItem = manager.getOrCreateTestItem(id) expect(testItem.id).to.eq(id) expect(testItem.parent?.id).to.eq(fileId) - let folderItem = testSuite.getTestItem('folder') - let fileItem = testSuite.getTestItem(fileId) + let folderItem = manager.getTestItem('folder') + let fileItem = manager.getTestItem(fileId) expect(folderItem?.children.size).to.eq(1) expect(fileItem?.children.size).to.eq(1) testUriMatches(folderItem!, path.resolve(config.getAbsoluteTestDirectory(), 'folder')) From 5489b4d4dae1661f79114ca77c28630f47a2e7d9 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 10:54:53 +0000 Subject: [PATCH 057/108] Stop interpolating strings in log messages, and other log tidying --- src/config.ts | 14 +++++------ src/main.ts | 7 +----- src/minitest/minitestTestRunner.ts | 3 ++- src/rspec/rspecTestRunner.ts | 2 +- src/testLoader.ts | 36 +++++++++++++++------------- src/testRunContext.ts | 18 +++++++------- src/testRunner.ts | 27 ++++++++++----------- test/stubs/stubTestItemCollection.ts | 4 ++-- 8 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/config.ts b/src/config.ts index 80a6fcc..031b9cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -109,7 +109,7 @@ export abstract class Config { * Detect the current test framework using 'bundle list'. */ private static detectTestFramework(log: IVSCodeExtLogger): string { - log.info(`Getting a list of Bundler dependencies with 'bundle list'.`); + log.info("Getting a list of Bundler dependencies with 'bundle list'."); const execArgs: childProcess.ExecOptions = { cwd: (vscode.workspace.workspaceFolders || [])[0].uri.fsPath, @@ -122,8 +122,8 @@ export abstract class Config { let err, stdout = childProcess.execSync('bundle list', execArgs); if (err) { - log.error(`Error while listing Bundler dependencies: ${err}`); - log.error(`Output: ${stdout}`); + log.error('Error while listing Bundler dependencies', err); + log.error('Output', stdout); throw err; } @@ -132,17 +132,17 @@ export abstract class Config { // Search for rspec or minitest in the output of 'bundle list'. // The search function returns the index where the string is found, or -1 otherwise. if (bundlerList.search('rspec-core') >= 0) { - log.info(`Detected RSpec test framework.`); + log.info('Detected RSpec test framework.'); return 'rspec'; } else if (bundlerList.search('minitest') >= 0) { - log.info(`Detected Minitest test framework.`); + log.info('Detected Minitest test framework.'); return 'minitest'; } else { - log.info(`Unable to automatically detect a test framework.`); + log.info('Unable to automatically detect a test framework.'); return 'none'; } } catch (error: any) { - log.error(error); + log.error('Error while detecting test suite', error); return 'none'; } } diff --git a/src/main.ts b/src/main.ts index 7930940..a849dec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,12 +11,7 @@ export const guessWorkspaceFolder = async (rootLog: IChildLogger) => { return undefined; } - console.debug("Found workspace folders:") - log.debug("Found workspace folders:") - for (const folder of vscode.workspace.workspaceFolders) { - console.debug(` - ${folder.uri.fsPath}`) - log.debug(` - ${folder.uri.fsPath}`) - } + log.debug("Found workspace folders", vscode.workspace.workspaceFolders) if (vscode.workspace.workspaceFolders.length < 2) { return vscode.workspace.workspaceFolders[0]; diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 86d9618..98e8d26 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -12,7 +12,8 @@ export class MinitestTestRunner extends TestRunner { * @param context Test run context */ handleStatus(test: any, context: TestRunContext): void { - this.log.debug(`Handling status of test: ${JSON.stringify(test)}`); + let log = this.log.getChildLogger({ label: "handleStatus" }) + log.trace("Handling status of test", test); let testItem = this.manager.getOrCreateTestItem(test.id) if (test.status === "passed") { context.passed(testItem) diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 1113653..56e7cca 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -14,7 +14,7 @@ export class RspecTestRunner extends TestRunner { */ handleStatus(test: ParsedTest, context: TestRunContext): void { let log = this.log.getChildLogger({ label: "handleStatus" }) - log.trace(`Handling status of test: ${JSON.stringify(test)}`); + log.trace("Handling status of test", test); let testItem = this.manager.getOrCreateTestItem(test.id) if (test.status === "passed") { log.trace("Passed", testItem.id) diff --git a/src/testLoader.ts b/src/testLoader.ts index a8645e8..c68ed96 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -35,7 +35,7 @@ export class TestLoader implements vscode.Disposable { private readonly resolveTestProfile: vscode.TestRunProfile, private readonly manager: TestSuiteManager, ) { - this.log = rootLog.getChildLogger({ label: "TestLoader" }); + this.log = rootLog.getChildLogger({ label: 'TestLoader' }); this.disposables.push(this.configWatcher()); } @@ -58,28 +58,33 @@ export class TestLoader implements vscode.Disposable { // When files are created, make sure there's a corresponding "file" node in the tree watcher.onDidCreate(uri => { - log.debug(`onDidCreate ${uri.fsPath}`) + let watcherLog = this.log.getChildLogger({label: 'onDidCreate watcher'}) + watcherLog.debug('File created', uri.fsPath) this.manager.getOrCreateTestItem(uri) }) // When files change, re-parse them. Note that you could optimize this so // that you only re-parse children that have been resolved in the past. watcher.onDidChange(uri => { - log.debug(`onDidChange ${uri.fsPath}`) + let watcherLog = this.log.getChildLogger({label: 'onDidChange watcher'}) + watcherLog.debug('File changed', uri.fsPath) + // TODO: batch these up somehow, else we'll spawn a ton of processes when, for + // example, changing branches in git this.parseTestsInFile(uri) }); // And, finally, delete TestItems for removed files. This is simple, since // we use the URI as the TestItem's ID. watcher.onDidDelete(uri => { - log.debug(`onDidDelete ${uri.fsPath}`) + let watcherLog = this.log.getChildLogger({label: 'onDidDelete watcher'}) + watcherLog.debug('File deleted', uri.fsPath) this.manager.deleteTestItem(uri) }); for (const file of await vscode.workspace.findFiles(pattern)) { - log.debug("Found file, creating TestItem", file) + log.debug('Found file, creating TestItem', file) this.manager.getOrCreateTestItem(file) } - log.debug("Resolving tests in found files") + log.debug('Resolving tests in found files') await this.loadTests() return watcher; @@ -92,14 +97,13 @@ export class TestLoader implements vscode.Disposable { public async discoverAllFilesInWorkspace(): Promise { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) let testDir = this.manager.config.getAbsoluteTestDirectory() - log.debug(`testDir: ${testDir}`) let patterns: Array = [] this.manager.config.getFilePattern().forEach(pattern => { patterns.push(new vscode.RelativePattern(testDir, '**/' + pattern)) }) - log.debug("Setting up watchers with the following test patterns", patterns) + log.debug('Setting up watchers with the following test patterns', patterns) return Promise.all(patterns.map(async (pattern) => await this.createWatcher(pattern))) } @@ -110,21 +114,21 @@ export class TestLoader implements vscode.Disposable { * @return The full test suite. */ public async loadTests(testItems?: vscode.TestItem[]): Promise { - let log = this.log.getChildLogger({label:"loadTests"}) - log.info(`Loading tests for ${testItems ? testItems.length : 'all'} items (${this.manager.config.frameworkName()})...`); + let log = this.log.getChildLogger({label:'loadTests'}) + log.info('Loading tests...', testItems?.map(x => x.id) || 'all tests'); try { let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) } catch (e: any) { - log.error("Failed to load tests", e) + log.error('Failed to load tests', e) return Promise.reject(e) } } public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { - let log = this.log.getChildLogger({label: "parseTestsInFile"}) + let log = this.log.getChildLogger({label: 'parseTestsInFile'}) let testItem: vscode.TestItem - if ("fsPath" in uri) { + if ('fsPath' in uri) { let test = this.manager.getTestItem(uri) if (!test) { return @@ -134,16 +138,16 @@ export class TestLoader implements vscode.Disposable { testItem = uri } - log.info(`${testItem.id} has been edited, reloading tests.`); + log.info('Test item has been changed, reloading tests.', testItem.id); await this.loadTests([testItem]) } private configWatcher(): vscode.Disposable { - let log = this.rootLog.getChildLogger({ label: "TestLoader.configWatcher" }); + let log = this.rootLog.getChildLogger({ label: 'TestLoader.configWatcher' }); log.debug('configWatcher') return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); - if (configChange.affectsConfiguration("rubyTestExplorer")) { + if (configChange.affectsConfiguration('rubyTestExplorer')) { this.manager.controller.items.replace([]) this.discoverAllFilesInWorkspace(); } diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 1c26415..e772671 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -37,7 +37,7 @@ export class TestRunContext { * @param test Test item to update. */ public enqueued(test: vscode.TestItem): void { - this.log.debug(`Enqueued: ${test.id}`) + this.log.debug('Enqueued test', test.id) this.testRun.enqueued(test) } @@ -64,11 +64,11 @@ export class TestRunContext { testItem.uri ?? vscode.Uri.file(file), new vscode.Position(line, 0) ) - this.log.debug(`Errored: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) - this.log.trace(`Error message: ${message}`) + this.log.debug('Errored test', test.id, file, line, duration) + this.log.trace('Error message', message) this.testRun.errored(testItem, testMessage, duration) } catch (e: any) { - this.log.error(`Failed to set test ${test} as Errored`, e) + this.log.error('Failed to report error state for test', test.id, e) } } @@ -93,8 +93,8 @@ export class TestRunContext { test.uri ?? vscode.Uri.file(file), new vscode.Position(line, 0) ) - this.log.debug(`Failed: ${test.id} (${file}:${line})${duration ? `, duration: ${duration}ms` : ''}`) - this.log.trace(`Failure message: ${message}`) + this.log.debug('Failed test', test.id, file, line, duration) + this.log.trace('Failure message', message) this.testRun.failed(test, testMessage, duration) } @@ -107,7 +107,7 @@ export class TestRunContext { public passed(test: vscode.TestItem, duration?: number | undefined ): void { - this.log.debug(`Passed: ${test.id}${duration ? `, duration: ${duration}ms` : ''}`) + this.log.debug('Passed test', test.id, duration) this.testRun.passed(test, duration) } @@ -117,7 +117,7 @@ export class TestRunContext { * @param test ID of the test item to update, or the test item. */ public skipped(test: vscode.TestItem): void { - this.log.debug(`Skipped: ${test.id}`) + this.log.debug('Skipped test', test.id) this.testRun.skipped(test) } @@ -127,7 +127,7 @@ export class TestRunContext { * @param test Test item to update, or the test item. */ public started(test: vscode.TestItem): void { - this.log.debug(`Started: ${test.id}`) + this.log.debug('Started test', test.id) this.testRun.started(test) } diff --git a/src/testRunner.ts b/src/testRunner.ts index 4e81f70..b414878 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -175,15 +175,13 @@ export abstract class TestRunner implements vscode.Disposable { token: vscode.CancellationToken, debuggerConfig?: vscode.DebugConfiguration ) { - let log = this.log.getChildLogger({ label: `runHandler` }) - - const queue: { context: TestRunContext, test: vscode.TestItem }[] = []; + let log = this.log.getChildLogger({ label: 'runHandler' }) if (debuggerConfig) { - log.debug(`Debugging test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); + log.debug('Debugging tests', request.include?.map(x => x.id)); if (this.workspace) { - log.error("Cannot debug without a folder opened") + log.error('Cannot debug without a folder opened') return } @@ -206,11 +204,11 @@ export abstract class TestRunner implements vscode.Disposable { }); } else { - log.debug(`Running test(s) ${JSON.stringify(request.include?.map(x => x.id))}`); + log.debug('Running test', request.include?.map(x => x.id)); } // Loop through all included tests, or all known tests, and add them to our queue - log.debug(`${request.include?.length || 0} tests in request`); + log.debug('Number of tests in request', request.include?.length || 0); let context = new TestRunContext( this.rootLog, token, @@ -265,7 +263,7 @@ export abstract class TestRunner implements vscode.Disposable { context.endTestRun(); } if (token.isCancellationRequested) { - log.info(`Test run aborted due to cancellation. ${queue.length} tests remain in queue`) + log.info('Test run aborted due to cancellation') } } @@ -309,8 +307,7 @@ export abstract class TestRunner implements vscode.Disposable { public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext): vscode.TestItem[] { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); - log.trace('Parsing the below JSON:'); - log.trace(`${testOutput}`); + log.trace('Parsing the below JSON:', testOutput); let testMetadata = JSON.parse(testOutput); let tests: Array = testMetadata.examples; @@ -348,12 +345,12 @@ export abstract class TestRunner implements vscode.Disposable { let newTestItem = this.manager.getOrCreateTestItem(test.id) newTestItem.canResolveChildren = !test.id.endsWith(']') - log.trace(`canResolveChildren (${test.id}): ${newTestItem.canResolveChildren}`) - log.trace(`Setting test ${newTestItem.id} label to "${description}"`) + log.trace('canResolveChildren', test.id, newTestItem.canResolveChildren) + log.trace('label', test.id, description) newTestItem.label = description newTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); parsedTests.push(newTestItem) - log.debug("Parsed test", test) + log.debug('Parsed test', test) if(context) { // Only handle status if actual test run, not dry run this.handleStatus(test, context); @@ -409,7 +406,7 @@ export abstract class TestRunner implements vscode.Disposable { */ protected async runTestFramework (testCommand: string, context: TestRunContext): Promise { context.token.onCancellationRequested(() => { - this.log.debug("Cancellation requested") + this.log.debug('Cancellation requested') this.killChild() }) @@ -419,7 +416,7 @@ export abstract class TestRunner implements vscode.Disposable { env: this.manager.config.getProcessEnv() }; - this.log.debug(`Running command: ${testCommand}`); + this.log.debug('Running command', testCommand); let testProcess = childProcess.spawn( testCommand, diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index f3612a4..9062d43 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -47,7 +47,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } add(item: vscode.TestItem): void { - this.log.debug(`Adding test ${item.id} to ${JSON.stringify(Object.keys(this.testIds))}`) + this.log.debug('Adding test to collection', item.id, Object.keys(this.testIds)) this.testIds[item.id] = item let sortedIds = Object.values(this.testIds).sort((a, b) => { if(a.id > b.id) return 1 @@ -62,7 +62,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } delete(itemId: string): void { - this.log.debug(`Deleting test ${itemId} from ${JSON.stringify(Object.keys(this.testIds))}`) + this.log.debug('Deleting test from collection', itemId, Object.keys(this.testIds)) delete this.testIds[itemId] } From 1bab95ea9aa0859666ed1cc00380b7a8a2d519dd Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 11:30:08 +0000 Subject: [PATCH 058/108] Move definition of ParsedTest to TestRunner where it's used --- src/testLoader.ts | 17 ----------------- src/testRunner.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/testLoader.ts b/src/testLoader.ts index c68ed96..0d90c24 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -2,23 +2,6 @@ import * as vscode from 'vscode'; import { IChildLogger } from '@vscode-logging/logger'; import { TestSuiteManager } from './testSuiteManager'; -export type ParsedTest = { - id: string, - full_description: string, - description: string, - file_path: string, - line_number: number, - location?: number, - status?: string, - pending_message?: string | null, - exception?: any, - type?: any, - full_path?: string, // Minitest - klass?: string, // Minitest - method?: string, // Minitest - runnable?: string, // Minitest -} - /** * Responsible for finding and watching test files, and loading tests from within those * files diff --git a/src/testRunner.ts b/src/testRunner.ts index b414878..bceaa6a 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -6,7 +6,24 @@ import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; -import { ParsedTest } from './testLoader'; + +// TODO - figure out which of these are RSpec only +type ParsedTest = { + id: string, + full_description: string, + description: string, + file_path: string, + line_number: number, + location?: number, + status?: string, + pending_message?: string | null, + exception?: any, + type?: any, + full_path?: string, // Minitest + klass?: string, // Minitest + method?: string, // Minitest + runnable?: string, // Minitest +} export abstract class TestRunner implements vscode.Disposable { protected currentChildProcess?: childProcess.ChildProcess; From 24300666412469db1a2e0ad77702ff85a43cd89d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 16:22:11 +0000 Subject: [PATCH 059/108] Fix imports after renaming TestSuite --- test/suite/minitest/minitest.test.ts | 2 +- test/suite/rspec/rspec.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 00dfa81..024322f 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { before, beforeEach } from 'mocha'; import { TestLoader } from '../../../src/testLoader'; -import { TestSuiteManager } from '../../testSuiteManager'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index ef8099e..a204018 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -5,7 +5,7 @@ import { before, beforeEach } from 'mocha'; import { expect } from 'chai'; import { TestLoader } from '../../../src/testLoader'; -import { TestSuiteManager } from '../../testSuiteManager'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; From 5871243fb65a14bc536af90daaf083f19e1e9644 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 6 Jan 2023 16:26:11 +0000 Subject: [PATCH 060/108] Split out parsed test types in TestRunner --- src/minitest/minitestTestRunner.ts | 20 +++++++++++++++++++- src/rspec/rspecTestRunner.ts | 15 ++++++++++++++- src/testRunner.ts | 5 ++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts index 98e8d26..39c50bd 100644 --- a/src/minitest/minitestTestRunner.ts +++ b/src/minitest/minitestTestRunner.ts @@ -1,6 +1,24 @@ import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; +// TODO - figure out which of these are RSpec only +type ParsedTest = { + id: string, + // full_description: string, + // description: string, + file_path: string, + line_number: number, + // location?: number, + status?: string, + pending_message?: string | null, + exception?: any, + // type?: any, + full_path?: string, // Minitest + klass?: string, // Minitest + method?: string, // Minitest + runnable?: string, // Minitest +} + export class MinitestTestRunner extends TestRunner { // Minitest notifies on test start canNotifyOnStartingTests: boolean = true @@ -11,7 +29,7 @@ export class MinitestTestRunner extends TestRunner { * @param test The test that we want to handle. * @param context Test run context */ - handleStatus(test: any, context: TestRunContext): void { + handleStatus(test: ParsedTest, context: TestRunContext): void { let log = this.log.getChildLogger({ label: "handleStatus" }) log.trace("Handling status of test", test); let testItem = this.manager.getOrCreateTestItem(test.id) diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts index 56e7cca..42b3d81 100644 --- a/src/rspec/rspecTestRunner.ts +++ b/src/rspec/rspecTestRunner.ts @@ -1,6 +1,19 @@ import { TestRunner } from '../testRunner'; import { TestRunContext } from '../testRunContext'; -import { ParsedTest } from 'src/testLoader'; + +// TODO - figure out which of these are RSpec only +type ParsedTest = { + id: string, + full_description: string, // RSpec + description: string, // RSpec + file_path: string, + line_number: number, + location?: number, // RSpec + status?: string, + pending_message?: string | null, + exception?: any, + type?: any // RSpec - presumably tag name/focus? +} export class RspecTestRunner extends TestRunner { // RSpec only notifies on test completion diff --git a/src/testRunner.ts b/src/testRunner.ts index bceaa6a..ac8d4e3 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -7,18 +7,17 @@ import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; -// TODO - figure out which of these are RSpec only type ParsedTest = { id: string, full_description: string, description: string, file_path: string, line_number: number, - location?: number, status?: string, pending_message?: string | null, exception?: any, - type?: any, + location?: number, // RSpec + type?: any // RSpec - presumably tag name/focus? full_path?: string, // Minitest klass?: string, // Minitest method?: string, // Minitest From bf0881c086ca583a658c270c99935c7e203f8d21 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 9 Jan 2023 18:23:26 +0000 Subject: [PATCH 061/108] Add queue for loading tests --- src/loaderQueue.ts | 134 +++++++++++++++++++++++++++ src/main.ts | 2 +- src/testLoader.ts | 92 +++++++++--------- test/suite/minitest/minitest.test.ts | 6 +- test/suite/rspec/rspec.test.ts | 5 +- 5 files changed, 191 insertions(+), 48 deletions(-) create mode 100644 src/loaderQueue.ts diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts new file mode 100644 index 0000000..ca54550 --- /dev/null +++ b/src/loaderQueue.ts @@ -0,0 +1,134 @@ +import * as vscode from 'vscode'; +import { IChildLogger } from '@vscode-logging/logger'; + +/** + * Type for items in the ResolveQueue + * item: The TestItem that is enqueued + * resolve: Function to call once this item has been loaded to resolve the associated promise + * 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, + resolve: () => void, + reject: (reason?: any) => void +} + +/** + * Queue for tests to be resolved/loaded + * + * When there are many files changed at once (e.g. during git checkouts/pulls, or when first loading all files in a + * project), we need to make sure that we don't spawn hundreds of test runners at once as that'll grind the computer + * to a halt. We can't change that fact that the async file resolvers notify us about files individually, so we use + * this queue to batch them up. + * + * When a test item is enqueued, the async worker function is woken up, drains the queue and runs all the enqueued + * items in a batch. While it is running, any additional items that are enqueued will sit in the queue. When the + * worker function finishes a batch, it will drain the queue again if there are items in it and run another batch, + * and if not it will wait until more items have been enqueued + */ +export class LoaderQueue implements vscode.Disposable { + private readonly log: IChildLogger + private readonly queue: QueueItem[] = [] + private isDisposed = false + private notifyQueueWorker?: () => void + private terminateQueueWorker?: () => void + public readonly worker: Promise + + constructor(rootLog: IChildLogger, private readonly processItems: (testItems?: vscode.TestItem[]) => Promise) { + this.log = rootLog.getChildLogger({label: 'ResolveQueue'}) + this.worker = this.resolveItemsInQueueWorker() + } + + /** + * Notifies the worker function that the queue is being disposed, so that it knows to stop processing items + * from the queue and that it must terminate, then waits for the worker function to finish + */ + dispose() { + // TODO: Terminate child process + this.log.info('disposed') + this.isDisposed = true + if (this.terminateQueueWorker) { + // Stop the worker function from waiting for more items + this.log.debug('notifying worker for disposal') + this.terminateQueueWorker() + } + // Wait for worker to finish + this.log.debug('waiting for worker to finish') + this.worker + .then(() => {this.log.info('worker promise resolved')}) + .catch((err) => {this.log.error('Error in worker', err)}) + } + + /** + * Enqueues a test item to be loaded + * + * @param item Test item to be loaded + * @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', item.id) + // Create queue item with empty functions + let queueItem: QueueItem = { + item: item, + resolve: () => {}, + reject: () => {}, + } + 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["reject"] = reject + }) + this.queue.push(queueItem) + if (this.notifyQueueWorker) { + this.log.debug('notifying worker of items in queue') + // Notify the worker function that there are items to resolve if it's waiting + this.notifyQueueWorker() + } + return itemPromise + } + + private async resolveItemsInQueueWorker(): Promise { + let log = this.log.getChildLogger({label: 'WorkerFunction'}) + log.info('worker started') + // Check to see if the queue is being disposed + while(!this.isDisposed) { + if (this.queue.length == 0) { + log.debug('awaiting items to resolve') + // While the queue is empty, wait for more items + await new Promise((resolve, reject) => { + // Set notification functions to the resolve/reject functions of this promise + this.notifyQueueWorker = async () => { + log.debug('received notification of items in queue') + resolve() + } + this.terminateQueueWorker = (reason?: any) => { + log.error('received rejection while waiting for items to be enqueued', reason) + reject(reason) + } + }) + // Clear notification functions before draining queue and processing items + this.notifyQueueWorker = undefined + this.terminateQueueWorker = undefined + } else { + // Drain queue to get batch of test items to load + let queueItems: QueueItem[] = [] + while(this.queue.length > 0) { queueItems.push(this.queue.pop()!) } + + 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"]()) + } catch (err) { + this.log.error("Error resolving tests from queue", err) + // Reject promises associated with items in queue that we were trying to load + queueItems.map(x => x["reject"](err)) + } + } + } + this.log.debug('worker finished') + } +} diff --git a/src/main.ts b/src/main.ts index a849dec..c11cdf3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,7 +101,7 @@ export async function activate(context: vscode.ExtensionContext) { await testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); } else if (test.id.endsWith(".rb")) { // Only parse files - await testLoaderFactory.getLoader().parseTestsInFile(test); + await testLoaderFactory.getLoader().loadTestItem(test); } }; } diff --git a/src/testLoader.ts b/src/testLoader.ts index 0d90c24..1878c3f 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { IChildLogger } from '@vscode-logging/logger'; import { TestSuiteManager } from './testSuiteManager'; +import { LoaderQueue } from './loaderQueue'; /** * Responsible for finding and watching test files, and loading tests from within those @@ -12,6 +13,7 @@ export class TestLoader implements vscode.Disposable { protected disposables: { dispose(): void }[] = []; private readonly log: IChildLogger; private readonly cancellationTokenSource = new vscode.CancellationTokenSource() + private readonly resolveQueue: LoaderQueue constructor( readonly rootLog: IChildLogger, @@ -19,6 +21,8 @@ export class TestLoader implements vscode.Disposable { private readonly manager: TestSuiteManager, ) { this.log = rootLog.getChildLogger({ label: 'TestLoader' }); + this.resolveQueue = new LoaderQueue(rootLog, async (testItems?: vscode.TestItem[]) => await this.loadTests(testItems)) + this.disposables.push(this.resolveQueue) this.disposables.push(this.configWatcher()); } @@ -30,52 +34,49 @@ export class TestLoader implements vscode.Disposable { } /** - * Create a file watcher that will update the test tree when: + * Create a file watcher that will update the test item tree when: * - A test file is created * - A test file is changed * - A test file is deleted + * @param pattern Glob pattern to use for the file watcher */ - async createWatcher(pattern: vscode.GlobPattern): Promise { - let log = this.log.getChildLogger({label: `createWatcher(${pattern.toString()})`}) + createFileWatcher(pattern: vscode.GlobPattern): vscode.FileSystemWatcher { const watcher = vscode.workspace.createFileSystemWatcher(pattern); - // When files are created, make sure there's a corresponding "file" node in the tree + // When files are created, make sure there's a corresponding "file" node in the test item tree watcher.onDidCreate(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidCreate watcher'}) watcherLog.debug('File created', uri.fsPath) this.manager.getOrCreateTestItem(uri) }) - // When files change, re-parse them. Note that you could optimize this so - // that you only re-parse children that have been resolved in the past. + // When files change, reload them watcher.onDidChange(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidChange watcher'}) - watcherLog.debug('File changed', uri.fsPath) - // TODO: batch these up somehow, else we'll spawn a ton of processes when, for - // example, changing branches in git - this.parseTestsInFile(uri) + watcherLog.debug('File changed, reloading tests', uri.fsPath) + let testItem = this.manager.getTestItem(uri) + if (!testItem) { + watcherLog.error('Unable to find test item for file', uri) + } else { + this.resolveQueue.enqueue(testItem) + } }); - // And, finally, delete TestItems for removed files. This is simple, since - // we use the URI as the TestItem's ID. + // And, finally, delete TestItems for removed files watcher.onDidDelete(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidDelete watcher'}) watcherLog.debug('File deleted', uri.fsPath) this.manager.deleteTestItem(uri) }); - for (const file of await vscode.workspace.findFiles(pattern)) { - log.debug('Found file, creating TestItem', file) - this.manager.getOrCreateTestItem(file) - } - - log.debug('Resolving tests in found files') - await this.loadTests() - return watcher; } /** - * Searches the configured test directory for test files, and calls createWatcher for - * each one found. + * Searches the configured test directory for test files, according to the configured glob patterns. + * + * For each pattern, a FileWatcher is created to notify the plugin of changes to files + * For each file found, a request to load tests from that file is enqueued + * + * Waits for all test files to be loaded from the queue before returning */ public async discoverAllFilesInWorkspace(): Promise { let log = this.log.getChildLogger({ label: `${this.discoverAllFilesInWorkspace.name}` }) @@ -87,16 +88,31 @@ export class TestLoader implements vscode.Disposable { }) log.debug('Setting up watchers with the following test patterns', patterns) - return Promise.all(patterns.map(async (pattern) => await this.createWatcher(pattern))) + let resolveFilesPromises: Promise[] = [] + let fileWatchers = await Promise.all(patterns.map(async (pattern) => { + for (const file of await vscode.workspace.findFiles(pattern)) { + log.debug('Found file, creating TestItem', file) + // Enqueue the file to load tests from it + resolveFilesPromises.push(this.resolveQueue.enqueue(this.manager.getOrCreateTestItem(file))) + } + + // TODO - skip if filewatcher for this pattern exists and dispose filewatchers for patterns no longer in config + let fileWatcher = this.createFileWatcher(pattern) + this.disposables.push(fileWatcher) + return fileWatcher + })) + await Promise.all(resolveFilesPromises) + return fileWatchers } /** - * Takes the output from initTests() and parses the resulting - * JSON into a TestSuiteInfo object. + * Runs the test runner using the 'ResolveTests' profile to load test information. + * + * Only called from the queue * - * @return The full test suite. + * @param testItems Array of test items to be loaded. If undefined, all tests and files are loaded */ - public async loadTests(testItems?: vscode.TestItem[]): Promise { + private async loadTests(testItems?: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:'loadTests'}) log.info('Loading tests...', testItems?.map(x => x.id) || 'all tests'); try { @@ -108,21 +124,13 @@ export class TestLoader implements vscode.Disposable { } } - public async parseTestsInFile(uri: vscode.Uri | vscode.TestItem) { - let log = this.log.getChildLogger({label: 'parseTestsInFile'}) - let testItem: vscode.TestItem - if ('fsPath' in uri) { - let test = this.manager.getTestItem(uri) - if (!test) { - return - } - testItem = test - } else { - testItem = uri - } - - log.info('Test item has been changed, reloading tests.', testItem.id); - await this.loadTests([testItem]) + /** + * Enqueues a single test item to be loaded + * @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) } private configWatcher(): vscode.Disposable { diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 024322f..a19981e 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -22,7 +22,6 @@ suite('Extension Test for Minitest', function() { let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; - //const logger = NOOP_LOGGER; const log = logger("info"); let expectedPath = (file: string): string => { @@ -90,7 +89,8 @@ suite('Extension Test for Minitest', function() { item.canResolveChildren = true return item } - testController.items.add(createTest("abs_test.rb")) + let absTestItem = createTest("abs_test.rb") + testController.items.add(absTestItem) let subfolderItem = createTest("square") testController.items.add(subfolderItem) subfolderItem.children.add(createTest("square/square_test.rb", "square_test.rb")) @@ -124,7 +124,7 @@ suite('Extension Test for Minitest', function() { ) // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_test.rb"))) + await testLoader.loadTestItem(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 a204018..dc8d1d2 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -86,7 +86,8 @@ suite('Extension Test for RSpec', function() { // Populate controller with test files. This would be done by the filesystem globs in the watchers let createTest = (id: string, label?: string) => testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) - testController.items.add(createTest("abs_spec.rb")) + let absSpecItem = createTest("abs_spec.rb") + testController.items.add(absSpecItem) let subfolderItem = createTest("square") testController.items.add(subfolderItem) subfolderItem.children.add(createTest("square/square_spec.rb", "square_spec.rb")) @@ -117,7 +118,7 @@ suite('Extension Test for RSpec', function() { ) // Resolve a file (e.g. by clicking on it in the test explorer) - await testLoader.parseTestsInFile(vscode.Uri.file(expectedPath("abs_spec.rb"))) + await testLoader.loadTestItem(absSpecItem) // Tests in that file have now been added to suite testItemCollectionMatches(testController.items, From 6dbcf185eb49a89bb7f96b2d6203a0ac81c4a8db Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 9 Jan 2023 23:35:21 +0000 Subject: [PATCH 062/108] Ensure disposables are all handled correctly & tidy debug session handling --- src/main.ts | 26 ++++++--- src/testFactory.ts | 58 ++++++++++++++++++- src/testLoader.ts | 9 ++- src/testRunner.ts | 138 +++++++++++++++++++++------------------------ 4 files changed, 145 insertions(+), 86 deletions(-) diff --git a/src/main.ts b/src/main.ts index c11cdf3..0dafe17 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,11 +32,13 @@ export const guessWorkspaceFolder = async (rootLog: IChildLogger) => { export async function activate(context: vscode.ExtensionContext) { let extensionConfig = vscode.workspace.getConfiguration('rubyTestExplorer', null) + let logOutputChannel = vscode.window.createOutputChannel("Ruby Test Explorer log") + context.subscriptions.push(logOutputChannel) const log = getExtensionLogger({ extName: "RubyTestExplorer", level: "debug", // See LogLevel type in @vscode-logging/types for possible logLevels logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` - logOutputChannel: vscode.window.createOutputChannel("Ruby Test Explorer log"), // OutputChannel for the logger + logOutputChannel: logOutputChannel, // OutputChannel for the logger sourceLocationTracking: false, logConsole: (extensionConfig.get('logPanel') as boolean) // define if messages should be logged to the consol }); @@ -64,12 +66,12 @@ export async function activate(context: vscode.ExtensionContext) { const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); // TODO: (?) Add a "Profile" profile for profiling tests - let profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { + const profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { // Default run profile for running tests runProfile: controller.createRunProfile( 'Run', vscode.TestRunProfileKind.Run, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + (request, token) => factory.getRunner().runHandler(request, token), true // Default run profile ), @@ -77,7 +79,7 @@ export async function activate(context: vscode.ExtensionContext) { resolveTestsProfile: controller.createRunProfile( 'ResolveTests', vscode.TestRunProfileKind.Run, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token), + (request, token) => factory.getRunner().runHandler(request, token), false ), @@ -85,23 +87,29 @@ export async function activate(context: vscode.ExtensionContext) { debugProfile: controller.createRunProfile( 'Debug', vscode.TestRunProfileKind.Debug, - (request, token) => testLoaderFactory.getRunner().runHandler(request, token, debuggerConfig), + (request, token) => factory.getRunner().runHandler(request, token, debuggerConfig), true ), } - const testLoaderFactory = new TestFactory(log, controller, testConfig, profiles, workspace); + const factory = new TestFactory(log, controller, testConfig, profiles, workspace); + + // Ensure disposables are registered with VSC to be disposed of when the extension is deactivated context.subscriptions.push(controller); + context.subscriptions.push(profiles.runProfile); + context.subscriptions.push(profiles.debugProfile); + context.subscriptions.push(profiles.resolveTestsProfile); + context.subscriptions.push(factory); - testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); + factory.getLoader().discoverAllFilesInWorkspace(); controller.resolveHandler = async test => { log.debug('resolveHandler called', test) if (!test) { - await testLoaderFactory.getLoader().discoverAllFilesInWorkspace(); + await factory.getLoader().discoverAllFilesInWorkspace(); } else if (test.id.endsWith(".rb")) { // Only parse files - await testLoaderFactory.getLoader().loadTestItem(test); + await factory.getLoader().loadTestItem(test); } }; } diff --git a/src/testFactory.ts b/src/testFactory.ts index 81a2afc..0ae8bb7 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -8,7 +8,13 @@ import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; import { TestSuiteManager } from './testSuiteManager'; +/** + * Factory for (re)creating {@link TestRunner} and {@link TestLoader} instances + * + * Also takes care of disposing them when required + */ export class TestFactory implements vscode.Disposable { + private isDisposed = false; private loader: TestLoader | null = null; private runner: RspecTestRunner | MinitestTestRunner | null = null; protected disposables: { dispose(): void }[] = []; @@ -28,13 +34,29 @@ export class TestFactory implements vscode.Disposable { } dispose(): void { + this.isDisposed = true for (const disposable of this.disposables) { - disposable.dispose(); + try { + this.log.debug('Disposing object', disposable) + disposable.dispose(); + } catch (error) { + this.log.error('Error when disposing object', disposable, error) + } } this.disposables = []; } + /** + * Returns the current {@link RspecTestRunner} or {@link MinitestTestRunner} instance + * + * If one does not exist, a new instance is created according to the current configured test framework, + * which is then returned + * + * @returns Either {@link RspecTestRunner} or {@link MinitestTestRunner} + * @throws if this factory has been disposed + */ public getRunner(): RspecTestRunner | MinitestTestRunner { + this.checkIfDisposed() if (!this.runner) { this.runner = this.framework == "rspec" ? new RspecTestRunner( @@ -52,7 +74,16 @@ export class TestFactory implements vscode.Disposable { return this.runner } + /** + * Returns the current {@link TestLoader} instance + * + * If one does not exist, a new instance is created which is then returned + * + * @returns {@link TestLoader} + * @throws if this factory has been disposed + */ public getLoader(): TestLoader { + this.checkIfDisposed() if (!this.loader) { this.loader = new TestLoader( this.log, @@ -64,10 +95,28 @@ export class TestFactory implements vscode.Disposable { return this.loader } + /** + * Helper method to check the current value of the isDisposed flag and to throw an error + * if it is set, to prevent objects being created after {@link dispose()} has been called + */ + private checkIfDisposed(): void { + if (this.isDisposed) { + throw new Error("Factory has been disposed") + } + } + + /** + * Registers a listener with VSC to be notified if the configuration is changed to use a different test framework. + * + * If an event is received, we dispose the current loader, runner and config so that they can be recreated + * for the new framework + * + * @returns A {@link Disposable} that is used to unregister the config watcher from receiving events + */ private configWatcher(): vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); - if (configChange.affectsConfiguration("rubyTestExplorer")) { + if (configChange.affectsConfiguration("rubyTestExplorer.testFramework")) { let newFramework = Config.getTestFramework(this.log); if (newFramework !== this.framework) { // Config has changed to a different framework - recreate test loader and runner @@ -87,6 +136,11 @@ export class TestFactory implements vscode.Disposable { }) } + /** + * Helper method to dispose of an object and remove it from the list of disposables + * + * @param instance the object to be disposed + */ private disposeInstance(instance: vscode.Disposable) { let index = this.disposables.indexOf(instance); if (index !== -1) { diff --git a/src/testLoader.ts b/src/testLoader.ts index 1878c3f..438a8c9 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -22,13 +22,18 @@ export class TestLoader implements vscode.Disposable { ) { this.log = rootLog.getChildLogger({ label: 'TestLoader' }); this.resolveQueue = new LoaderQueue(rootLog, async (testItems?: vscode.TestItem[]) => await this.loadTests(testItems)) + this.disposables.push(this.cancellationTokenSource) this.disposables.push(this.resolveQueue) this.disposables.push(this.configWatcher()); } dispose(): void { for (const disposable of this.disposables) { - disposable.dispose(); + try { + disposable.dispose(); + } catch (err) { + this.log.error("Error disposing object", disposable, err) + } } this.disposables = []; } @@ -42,6 +47,7 @@ export class TestLoader implements vscode.Disposable { */ createFileWatcher(pattern: vscode.GlobPattern): vscode.FileSystemWatcher { const watcher = vscode.workspace.createFileSystemWatcher(pattern); + this.disposables.push(watcher) // When files are created, make sure there's a corresponding "file" node in the test item tree watcher.onDidCreate(uri => { @@ -98,7 +104,6 @@ export class TestLoader implements vscode.Disposable { // TODO - skip if filewatcher for this pattern exists and dispose filewatchers for patterns no longer in config let fileWatcher = this.createFileWatcher(pattern) - this.disposables.push(fileWatcher) return fileWatcher })) await Promise.all(resolveFilesPromises) diff --git a/src/testRunner.ts b/src/testRunner.ts index ac8d4e3..6940ba0 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -48,7 +48,11 @@ export abstract class TestRunner implements vscode.Disposable { public dispose() { this.killChild(); for (const disposable of this.disposables) { - disposable.dispose(); + try { + disposable.dispose(); + } catch (err) { + this.log.error('Error disposing object', err) + } } this.disposables = []; } @@ -193,36 +197,6 @@ export abstract class TestRunner implements vscode.Disposable { ) { let log = this.log.getChildLogger({ label: 'runHandler' }) - if (debuggerConfig) { - log.debug('Debugging tests', request.include?.map(x => x.id)); - - if (this.workspace) { - log.error('Cannot debug without a folder opened') - return - } - - this.log.info('Starting the debug session'); - let debugSession: any; - try { - await this.debugCommandStarted() - debugSession = await this.startDebugging(debuggerConfig); - } catch (err) { - log.error('Failed starting the debug session - aborting', err); - this.killChild(); - return; - } - - const subscription = this.onDidTerminateDebugSession((session) => { - if (debugSession != session) return; - log.info('Debug session ended'); - this.killChild(); // terminate the test run - subscription.dispose(); - }); - } - else { - log.debug('Running test', request.include?.map(x => x.id)); - } - // Loop through all included tests, or all known tests, and add them to our queue log.debug('Number of tests in request', request.include?.length || 0); let context = new TestRunContext( @@ -268,7 +242,14 @@ export abstract class TestRunner implements vscode.Disposable { log.trace("Current command", command) } } - await this.runTestFramework(command, context) + if (debuggerConfig) { + log.debug('Debugging tests', request.include?.map(x => x.id)); + await Promise.all([this.startDebugSession(debuggerConfig), this.runTestFramework(command, context)]) + } + else { + log.debug('Running test', request.include?.map(x => x.id)); + await this.runTestFramework(command, context) + } } catch (err) { log.error("Error running tests", err) @@ -283,43 +264,57 @@ export abstract class TestRunner implements vscode.Disposable { } } - private async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { - const debugSessionPromise = new Promise((resolve, reject) => { + private async startDebugSession(debuggerConfig: vscode.DebugConfiguration): Promise { + let log = this.log.getChildLogger({label: 'startDebugSession'}) - let subscription: vscode.Disposable | undefined; - subscription = vscode.debug.onDidStartDebugSession(debugSession => { - if ((debugSession.name === debuggerConfig.name) && subscription) { - resolve(debugSession); - subscription.dispose(); - subscription = undefined; - } - }); + if (this.workspace) { + log.error('Cannot debug without a folder opened') + return + } - setTimeout(() => { - if (subscription) { - reject(new Error('Debug session failed to start within 5 seconds')); - subscription.dispose(); - subscription = undefined; + this.log.info('Starting the debug session'); + + const debugCommandStartedPromise = new Promise((resolve, _) => { + this.debugCommandStartedResolver = resolve + }) + try { + let activeDebugSession: vscode.DebugSession + const debugStartSessionSubscription = vscode.debug.onDidStartDebugSession(debugSession => { + if (debugSession.name === debuggerConfig.name) { + log.info('Debug session started', debugSession.name); + activeDebugSession = debugSession } - }, 5000); - }); + }) + try { + await Promise.race( + [ + // Wait for either timeout or for the child process to notify us that it has started + debugCommandStartedPromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('Debug session failed to start within 5 seconds')), 5000)) + ] + ) + } finally { + debugStartSessionSubscription.dispose() + } - if (!this.workspace) { - throw new Error("Cannot debug without a folder open") - } + const debugSessionStarted = await vscode.debug.startDebugging(this.workspace, debuggerConfig); + if (!debugSessionStarted) { + throw new Error('Debug session failed to start') + } - const started = await vscode.debug.startDebugging(this.workspace, debuggerConfig); - if (started) { - return await debugSessionPromise; - } else { - throw new Error('Debug session couldn\'t be started'); + const debugStopSubscription = vscode.debug.onDidTerminateDebugSession(session => { + if (session === activeDebugSession) { + log.info('Debug session ended', session.name); + this.killChild(); // terminate the test run + debugStopSubscription.dispose(); + } + }) + } catch (err) { + log.error('Error starting debug session', err) + this.killChild() } } - private onDidTerminateDebugSession(cb: (session: vscode.DebugSession) => any): vscode.Disposable { - return vscode.debug.onDidTerminateDebugSession(cb); - } - public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext): vscode.TestItem[] { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = TestRunner.getJsonFromOutput(testOutput); @@ -390,13 +385,6 @@ export abstract class TestRunner implements vscode.Disposable { return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); } - public async debugCommandStarted(): Promise { - return new Promise(async (resolve, reject) => { - this.debugCommandStartedResolver = resolve; - setTimeout(() => { reject("debugCommandStarted timed out") }, 10000) - }) - } - /** * Mark a test node and all its children as being queued for execution */ @@ -420,11 +408,15 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - protected async runTestFramework (testCommand: string, context: TestRunContext): Promise { - context.token.onCancellationRequested(() => { - this.log.debug('Cancellation requested') - this.killChild() - }) + private async runTestFramework (testCommand: string, context: TestRunContext): Promise { + context.token.onCancellationRequested( + () => { + this.log.debug('Cancellation requested') + this.killChild() + }, + this, + this.disposables + ) const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, From 53edadd9a484e6e4516db8392f6bcd218a6d9afe Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 10 Jan 2023 14:56:53 +0000 Subject: [PATCH 063/108] Combine test runners and begin to unify test output format Not yet tested --- ruby/custom_formatter.rb | 19 +- ruby/vscode/minitest/reporter.rb | 12 +- src/frameworkProcess.ts | 318 +++++++++++++ src/minitest/minitestTestRunner.ts | 79 --- src/rspec/rspecTestRunner.ts | 87 ---- src/testFactory.ts | 28 +- src/testRunContext.ts | 4 +- src/testRunner.ts | 255 ++-------- src/testStatus.ts | 18 + src/testSuiteManager.ts | 50 +- test/suite/minitest/minitest.test.ts | 8 +- test/suite/rspec/rspec.test.ts | 8 +- test/suite/unitTests/frameworkProcess.test.ts | 255 ++++++++++ test/suite/unitTests/testRunner.test.ts | 450 ++++++++---------- 14 files changed, 895 insertions(+), 696 deletions(-) create mode 100644 src/frameworkProcess.ts delete mode 100644 src/minitest/minitestTestRunner.ts delete mode 100644 src/rspec/rspecTestRunner.ts create mode 100644 src/testStatus.ts create mode 100644 test/suite/unitTests/frameworkProcess.test.ts diff --git a/ruby/custom_formatter.rb b/ruby/custom_formatter.rb index d4d1b86..4855f1b 100644 --- a/ruby/custom_formatter.rb +++ b/ruby/custom_formatter.rb @@ -47,7 +47,8 @@ def stop(notification) hash[:exception] = { class: e.class.name, message: e.message, - backtrace: e.backtrace + backtrace: e.backtrace, + position: exception_position(e.backtrace_locations, example.metadata) } end end @@ -69,13 +70,15 @@ def example_passed(notification) end def example_failed(notification) - output.write "FAILED: #{notification.example.id}\n" + klass = notification.example.exception.class + status = "#{klass.startsWith('RSpec') ? 'FAILED' : 'ERRORED'}: " + output.write "#{status}: #{notification.example.id}\n" # This isn't exposed for simplicity, need to figure out how to handle this later. # output.write "#{notification.exception.backtrace.to_json}\n" end def example_pending(notification) - output.write "PENDING: #{notification.example.id}\n" + output.write "SKIPPED: #{notification.example.id}\n" end private @@ -106,7 +109,15 @@ def format_example(example) file_path: example.metadata[:file_path], line_number: example.metadata[:line_number], type: example.metadata[:type], - pending_message: example.execution_result.pending_message + pending_message: example.execution_result.pending_message, + duration: example.execution_result.run_time } end + + def exception_position(backtrace, metadata) + location = backtrace.find { |frame| frame.path.end_with?(metadata[:file]) } + return metadata[:line_number] unless location + + location.lineno + end end diff --git a/ruby/vscode/minitest/reporter.rb b/ruby/vscode/minitest/reporter.rb index 1b5fcaf..9f13a87 100644 --- a/ruby/vscode/minitest/reporter.rb +++ b/ruby/vscode/minitest/reporter.rb @@ -57,7 +57,8 @@ def vscode_data pending_count: skips, errors_outside_of_examples_count: errors }, - summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", + summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, " \ + "Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", examples: results.map { |r| vscode_result(r) } } end @@ -65,14 +66,14 @@ def vscode_data def vscode_result(r) base = VSCode::Minitest.tests.find_by(klass: r.klass, method: r.name).dup if r.skipped? - base[:status] = "failed" + base[:status] = 'skipped' base[:pending_message] = r.failure.message elsif r.passed? - base[:status] = "passed" + base[:status] = 'passed' else - base[:status] = "failed" - base[:pending_message] = nil e = r.failure.exception + base[:status] = e.class.instance_of?('Minitest::UnexpectedError') ? 'errored' : 'failed' + base[:pending_message] = nil backtrace = expand_backtrace(e.backtrace) base[:exception] = { class: e.class.name, @@ -82,6 +83,7 @@ def vscode_result(r) position: exception_position(backtrace, base[:full_path]) || base[:line_number] } end + base[:duration] = r.time base end diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts new file mode 100644 index 0000000..8eb832b --- /dev/null +++ b/src/frameworkProcess.ts @@ -0,0 +1,318 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import split2 from 'split2'; +import { IChildLogger } from '@vscode-logging/logger'; +import { Status, TestStatus } from './testStatus'; +import { TestRunContext } from './testRunContext'; +import { TestSuiteManager } from './testSuiteManager'; + +type ParsedTest = { + id: string, + full_description: string, + description: string, + file_path: string, + line_number: number, + duration: number, + status?: string, + pending_message?: string | null, + exception?: any, + location?: number, // RSpec + type?: any // RSpec - presumably tag name/focus? + full_path?: string, // Minitest + klass?: string, // Minitest + method?: string, // Minitest + runnable?: string, // Minitest +} + +export class FrameworkProcess implements vscode.Disposable { + private childProcess?: childProcess.ChildProcess; + protected readonly log: IChildLogger; + private readonly disposables: vscode.Disposable[] = [] + private isDisposed = false; + private readonly testStatusEmitter: vscode.EventEmitter = new vscode.EventEmitter() + private readonly statusPattern = new RegExp(/(?RUNNING|PASSED|FAILED|ERRORED|SKIPPED): (?.*)/) + + constructor( + readonly rootLog: IChildLogger, + private readonly testCommand: string, + private readonly spawnArgs: childProcess.SpawnOptions, + private readonly testContext: TestRunContext, + private readonly testManager: TestSuiteManager, + ) { + this.log = rootLog.getChildLogger({label: 'FrameworkProcess'}) + this.disposables.push(this.testContext.cancellationToken.onCancellationRequested(() => { + this.log.debug('Cancellation requested') + this.dispose() + })) + } + + dispose() { + this.isDisposed = true + this.childProcess?.kill() + for (const disposable of this.disposables) { + try { + disposable.dispose() + } catch (err) { + this.log.error('Error disposing object', disposable, err) + } + } + } + + public async startProcess( + onDebugStarted?: (value: void | PromiseLike) => void, + ) { + if (this.isDisposed) { + return + } + + try { + this.childProcess = childProcess.spawn( + this.testCommand, + this.spawnArgs + ) + + this.childProcess.stderr!.pipe(split2()).on('data', (data) => { + data = data.toString(); + this.log.trace(data); + if (data.startsWith('Fast Debugger') && onDebugStarted) { + onDebugStarted() + } + }) + + let outputParsedResolver: (value: void | PromiseLike) => void + let outputParsedRejecter: (reason?: any) => void + let outputParsedPromise = new Promise((resolve, reject) => { + outputParsedResolver = resolve + outputParsedRejecter = reject + }) + process.stdout!.pipe(split2()).on('data', (data) => { + this.onDataReceived(data, outputParsedResolver, outputParsedRejecter) + }); + + return (await Promise.all([ + new Promise<{code:number, signal:string}>((resolve, reject) => { + process.once('exit', (code: number, signal: string) => { + this.log.trace('Child process exited', code, signal) + }); + process.once('close', (code: number, signal: string) => { + this.log.debug('Child process exited, and all streams closed', code, signal) + resolve({code, signal}); + }); + process.once('error', (err: Error) => { + this.log.debug('Error event from child process', err.message) + reject(err); + }); + }), + outputParsedPromise + ]))[0] + } finally { + this.dispose() + } + } + + private onDataReceived( + data: string, + outputParsedResolver: (value: void | PromiseLike) => void, + outputParsedRejecter: (reason?: any) => void + ): void { + let log = this.log.getChildLogger({label: 'onDataReceived'}) + + let getTest = (testId: string): vscode.TestItem => { + testId = this.testManager.normaliseTestId(testId) + return this.testManager.getOrCreateTestItem(testId) + } + try { + if (data.includes('START_OF_TEST_JSON')) { + log.trace("Received test run results", data); + this.parseAndHandleTestOutput(data); + outputParsedResolver() + } else { + const match = this.statusPattern.exec(data) + if (match && match.groups) { + const id = match.groups['id'] + const status = match.groups['status'] + this.testStatusEmitter.fire( + new TestStatus( + getTest(id), + Status[status.toLocaleLowerCase() as keyof typeof Status] + ) + ) + } else { + log.trace("Ignoring unrecognised output", data) + } + } + } catch (err) { + log.error('Error parsing output', err) + outputParsedRejecter(err) + } + } + + private parseAndHandleTestOutput(testOutput: string): vscode.TestItem[] { + let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) + testOutput = this.getJsonFromOutput(testOutput); + log.trace('Parsing the below JSON:', testOutput); + let testMetadata = JSON.parse(testOutput); + let tests: Array = testMetadata.examples; + + let existingContainers: vscode.TestItem[] = [] + let parsedTests: vscode.TestItem[] = [] + if (tests && tests.length > 0) { + tests.forEach((test: ParsedTest) => { + test.id = this.testManager.normaliseTestId(test.id) + let itemAlreadyExists = true + let testItem = this.testManager.getOrCreateTestItem(test.id, (item) => { if (item.id == test.id) itemAlreadyExists = false }) + + testItem.canResolveChildren = !test.id.endsWith(']') + log.trace('canResolveChildren', test.id, testItem.canResolveChildren) + + testItem.description = this.parseDescription(test) + testItem.label = testItem.description + log.trace('label', test.id, testItem.description) + + testItem.range = this.parseRange(test) + + parsedTests.push(testItem) + if (testItem.canResolveChildren && itemAlreadyExists) { + existingContainers.push(testItem) + } + log.debug('Parsed test', test) + + this.handleStatus(testItem, test) + }); + for (const testFile of existingContainers) { + /* + * If a container test item (file, folder, suite, etc) already existed and was part of this test run (which + * means that we can be sure all its children are in the test run output) then replace any children it had + * with only the children that were in the test run output + * + * This means that when tests are removed from collections, they will be removed from the test suite + */ + testFile.children.replace(parsedTests.filter(x => !x.canResolveChildren && x.parent == testFile)) + } + } + return [] + } + + private parseDescription(test: ParsedTest): string { + // RSpec provides test ids like "file_name.rb[1:2:3]". + // This uses the digits at the end of the id to create + // an array of numbers representing the location of the + // test in the file. + let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); + let testNumber = test_location_array[test_location_array.length - 1]; + test.file_path = this.testManager.normaliseTestId(test.file_path).replace(/\[.*/, '') + + // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" + // is appended to the test description to distinguish between separate tests. + let description = test.description.startsWith('example at ') + ? `${test.full_description}test #${testNumber}` + : test.full_description; + + let currentFileLabel = test.file_path.split(path.sep).slice(-1)[0] + let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); + // If the current file label doesn't have a slash in it and it starts with the PascalCase'd + // file name, remove the from the start of the description. This turns, e.g. + // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. + if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { + // Optional check for a space following the PascalCase file name. In some + // cases, e.g. 'FileName#method_name` there's no space after the file name. + let regexString = `${pascalCurrentFileLabel}[ ]?`; + let regex = new RegExp(regexString, "g"); + description = description.replace(regex, ''); + } + return description + } + + private parseRange(test: ParsedTest): vscode.Range { + // TODO: get end line numbers of tests, as well as start/end columns + let zeroBasedStartLineNumber = test.line_number - 1 + return new vscode.Range(zeroBasedStartLineNumber, 0, zeroBasedStartLineNumber, 0); + } + + /** + * Handles test state based on the output returned by the custom RSpec formatter. + * + * @param parsedtest The test that we want to handle. + * @param context Test run context + */ + handleStatus(testItem: vscode.TestItem, parsedtest: ParsedTest): void { + const log = this.log.getChildLogger({ label: "handleStatus" }) + log.trace("Handling status of test", parsedtest); + const status = Status[parsedtest.status as keyof typeof Status] + switch (status) { + case Status.skipped: + this.testStatusEmitter.fire(new TestStatus(testItem, status)) + break; + case Status.passed: + this.testStatusEmitter.fire(new TestStatus(testItem, status, parsedtest.duration)) + break; + case Status.failed: + case Status.errored: + this.testStatusEmitter.fire(new TestStatus(testItem, status, parsedtest.duration, this.failureMessage(testItem, parsedtest))) + break; + default: + log.error('Unexpected test status', status, testItem.id) + } + } + + private failureMessage(testItem: vscode.TestItem, parsedTest: ParsedTest): vscode.TestMessage { + // Remove linebreaks from error message. + let errorMessageNoLinebreaks = parsedTest.exception.message.replace(/(\r\n|\n|\r)/, ' '); + // Prepend the class name to the error message string. + let errorMessage: string = `${parsedTest.exception.class}:\n${errorMessageNoLinebreaks}`; + + let errorMessageLine: number | undefined; + + // Add backtrace to errorMessage if it exists. + if (parsedTest.exception.backtrace) { + errorMessage += `\n\nBacktrace:\n`; + parsedTest.exception.backtrace.forEach((line: string) => { + errorMessage += `${line}\n`; + }); + } + + if (parsedTest.exception.position) { + errorMessageLine = parsedTest.exception.position; + } + + let testMessage = new vscode.TestMessage(errorMessage) + testMessage.location = new vscode.Location( + testItem.uri!, + new vscode.Position(errorMessageLine || testItem.range!.start.line, 0) + ) + return testMessage + } + + /** + * Convert a string from snake_case to PascalCase. + * Note that the function will return the input string unchanged if it + * includes a '/'. + * + * @param string The string to convert to PascalCase. + * @return The converted string. + */ + private snakeToPascalCase(string: string): string { + if (string.includes('/')) { return string } + return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); + } + + /** + * Pull JSON out of the test framework output. + * + * RSpec and Minitest frequently return bad data even when they're told to + * format the output as JSON, e.g. due to code coverage messages and other + * injections from gems. This gets the JSON by searching for + * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing + * curly brace and `END_OF_TEST_JSON`. These are output by the custom + * RSpec formatter or Minitest Rake task as part of the final JSON output. + * + * @param output The output returned by running a command. + * @return A string representation of the JSON found in the output. + */ + private getJsonFromOutput(output: string): string { + output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); + // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. + return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); + } +} diff --git a/src/minitest/minitestTestRunner.ts b/src/minitest/minitestTestRunner.ts deleted file mode 100644 index 39c50bd..0000000 --- a/src/minitest/minitestTestRunner.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { TestRunner } from '../testRunner'; -import { TestRunContext } from '../testRunContext'; - -// TODO - figure out which of these are RSpec only -type ParsedTest = { - id: string, - // full_description: string, - // description: string, - file_path: string, - line_number: number, - // location?: number, - status?: string, - pending_message?: string | null, - exception?: any, - // type?: any, - full_path?: string, // Minitest - klass?: string, // Minitest - method?: string, // Minitest - runnable?: string, // Minitest -} - -export class MinitestTestRunner extends TestRunner { - // Minitest notifies on test start - canNotifyOnStartingTests: boolean = true - - /** - * Handles test state based on the output returned by the Minitest Rake task. - * - * @param test The test that we want to handle. - * @param context Test run context - */ - handleStatus(test: ParsedTest, context: TestRunContext): void { - let log = this.log.getChildLogger({ label: "handleStatus" }) - log.trace("Handling status of test", test); - let testItem = this.manager.getOrCreateTestItem(test.id) - if (test.status === "passed") { - context.passed(testItem) - } else if (test.status === "failed" && test.pending_message === null) { - // Failed/Errored - let errorMessageLine: number = test.line_number; - let errorMessage: string = test.exception.message; - - if (test.exception.position) { - errorMessageLine = test.exception.position; - } - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - errorMessage += `\n\nFull Backtrace:\n`; - test.exception.full_backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - }); - } - - if (test.exception.class === "Minitest::UnexpectedError") { - context.errored( - testItem, - errorMessage, - test.file_path.replace('./', ''), - errorMessageLine - 1 - ) - } else { - context.failed( - testItem, - errorMessage, - test.file_path.replace('./', ''), - errorMessageLine - 1 - ) - } - } else if (test.status === "failed" && test.pending_message !== null) { - // Handle pending test cases. - context.skipped(testItem) - } - }; -} diff --git a/src/rspec/rspecTestRunner.ts b/src/rspec/rspecTestRunner.ts deleted file mode 100644 index 42b3d81..0000000 --- a/src/rspec/rspecTestRunner.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { TestRunner } from '../testRunner'; -import { TestRunContext } from '../testRunContext'; - -// TODO - figure out which of these are RSpec only -type ParsedTest = { - id: string, - full_description: string, // RSpec - description: string, // RSpec - file_path: string, - line_number: number, - location?: number, // RSpec - status?: string, - pending_message?: string | null, - exception?: any, - type?: any // RSpec - presumably tag name/focus? -} - -export class RspecTestRunner extends TestRunner { - // RSpec only notifies on test completion - canNotifyOnStartingTests: boolean = false - - /** - * Handles test state based on the output returned by the custom RSpec formatter. - * - * @param test The test that we want to handle. - * @param context Test run context - */ - handleStatus(test: ParsedTest, context: TestRunContext): void { - let log = this.log.getChildLogger({ label: "handleStatus" }) - log.trace("Handling status of test", test); - let testItem = this.manager.getOrCreateTestItem(test.id) - if (test.status === "passed") { - log.trace("Passed", testItem.id) - context.passed(testItem) - } else if (test.status === "failed" && test.pending_message === null) { - log.trace("Failed/Errored", testItem.id) - // Remove linebreaks from error message. - let errorMessageNoLinebreaks = test.exception.message.replace(/(\r\n|\n|\r)/, ' '); - // Prepend the class name to the error message string. - let errorMessage: string = `${test.exception.class}:\n${errorMessageNoLinebreaks}`; - - let fileBacktraceLineNumber: number | undefined; - - let filePath = test.file_path.replace('./', ''); - - // Add backtrace to errorMessage if it exists. - if (test.exception.backtrace) { - errorMessage += `\n\nBacktrace:\n`; - test.exception.backtrace.forEach((line: string) => { - errorMessage += `${line}\n`; - // If the backtrace line includes the current file path, try to get the line number from it. - if (line.includes(filePath)) { - let filePathArray = filePath.split('/'); - let fileName = filePathArray[filePathArray.length - 1]; - // Input: spec/models/game_spec.rb:75:in `block (3 levels) in - // Output: 75 - let regex = new RegExp(`${fileName}\:(\\d+)`); - let match = line.match(regex); - if (match && match[1]) { - fileBacktraceLineNumber = parseInt(match[1]); - } - } - }); - } - - if (test.exception.class.startsWith("RSpec")) { - context.failed( - testItem, - errorMessage, - filePath, - (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, - ) - } else { - context.errored( - testItem, - errorMessage, - filePath, - (fileBacktraceLineNumber ? fileBacktraceLineNumber : test.line_number) - 1, - ) - } - } else if ((test.status === "pending" || test.status === "failed") && test.pending_message !== null) { - // Handle pending test cases. - log.trace("Skipped", testItem.id) - context.skipped(testItem) - } - }; -} diff --git a/src/testFactory.ts b/src/testFactory.ts index 0ae8bb7..6508948 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -1,12 +1,11 @@ import * as vscode from 'vscode'; import { IVSCodeExtLogger } from "@vscode-logging/logger"; -import { RspecTestRunner } from './rspec/rspecTestRunner'; -import { MinitestTestRunner } from './minitest/minitestTestRunner'; import { Config } from './config'; import { TestLoader } from './testLoader'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; import { TestSuiteManager } from './testSuiteManager'; +import { TestRunner } from './testRunner'; /** * Factory for (re)creating {@link TestRunner} and {@link TestLoader} instances @@ -16,7 +15,7 @@ import { TestSuiteManager } from './testSuiteManager'; export class TestFactory implements vscode.Disposable { private isDisposed = false; private loader: TestLoader | null = null; - private runner: RspecTestRunner | MinitestTestRunner | null = null; + private runner: TestRunner | null = null; protected disposables: { dispose(): void }[] = []; protected framework: string; private manager: TestSuiteManager @@ -47,28 +46,23 @@ export class TestFactory implements vscode.Disposable { } /** - * Returns the current {@link RspecTestRunner} or {@link MinitestTestRunner} instance + * Returns the current {@link TestRunner} instance * * If one does not exist, a new instance is created according to the current configured test framework, * which is then returned * - * @returns Either {@link RspecTestRunner} or {@link MinitestTestRunner} + * @returns The current {@link TestRunner} instance * @throws if this factory has been disposed */ - public getRunner(): RspecTestRunner | MinitestTestRunner { + public getRunner(): TestRunner { this.checkIfDisposed() if (!this.runner) { - this.runner = this.framework == "rspec" - ? new RspecTestRunner( - this.log, - this.manager, - this.workspace, - ) - : new MinitestTestRunner( - this.log, - this.manager, - this.workspace, - ) + this.runner = new TestRunner( + this.log, + this.manager, + this.framework == "minitest", + this.workspace, + ) this.disposables.push(this.runner); } return this.runner diff --git a/src/testRunContext.ts b/src/testRunContext.ts index e772671..67040f7 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -14,14 +14,14 @@ export class TestRunContext { * Create a new context * * @param log Logger - * @param token Cancellation token triggered when the user cancels a test operation + * @param cancellationToken Cancellation token triggered when the user cancels a test operation * @param request Test run request for creating test run object * @param controller Test controller to look up tests for status reporting * @param debuggerConfig A VS Code debugger configuration. */ constructor( readonly rootLog: IChildLogger, - public readonly token: vscode.CancellationToken, + public readonly cancellationToken: vscode.CancellationToken, readonly request: vscode.TestRunRequest, readonly controller: vscode.TestController, public readonly debuggerConfig?: vscode.DebugConfiguration diff --git a/src/testRunner.ts b/src/testRunner.ts index 6940ba0..32c7105 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -1,34 +1,16 @@ import * as vscode from 'vscode'; -import * as path from 'path'; import * as childProcess from 'child_process'; -import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; +import { FrameworkProcess } from './frameworkProcess'; -type ParsedTest = { - id: string, - full_description: string, - description: string, - file_path: string, - line_number: number, - status?: string, - pending_message?: string | null, - exception?: any, - location?: number, // RSpec - type?: any // RSpec - presumably tag name/focus? - full_path?: string, // Minitest - klass?: string, // Minitest - method?: string, // Minitest - runnable?: string, // Minitest -} - -export abstract class TestRunner implements vscode.Disposable { - protected currentChildProcess?: childProcess.ChildProcess; - protected debugCommandStartedResolver?: Function; +export class TestRunner implements vscode.Disposable { + protected debugCommandStartedResolver?: () => void; protected disposables: { dispose(): void }[] = []; protected readonly log: IChildLogger; + private readonly testProcessMap: Map /** * @param rootLog The Test Adapter logger, for logging. @@ -38,15 +20,14 @@ export abstract class TestRunner implements vscode.Disposable { constructor( readonly rootLog: IChildLogger, protected manager: TestSuiteManager, + private readonly canNotifyOnStartingTests: boolean, protected workspace?: vscode.WorkspaceFolder, ) { this.log = rootLog.getChildLogger({label: "TestRunner"}) + this.testProcessMap = new Map() } - abstract canNotifyOnStartingTests: boolean - public dispose() { - this.killChild(); for (const disposable of this.disposables) { try { disposable.dispose(); @@ -58,31 +39,19 @@ export abstract class TestRunner implements vscode.Disposable { } /** - * Kills the current child process if one exists. - */ - public killChild(): void { - if (this.currentChildProcess) { - this.currentChildProcess.kill(); - } - } - - /** - * Pull JSON out of the test framework output. - * - * RSpec and Minitest frequently return bad data even when they're told to - * format the output as JSON, e.g. due to code coverage messages and other - * injections from gems. This gets the JSON by searching for - * `START_OF_TEST_JSON` and an opening curly brace, as well as a closing - * curly brace and `END_OF_TEST_JSON`. These are output by the custom - * RSpec formatter or Minitest Rake task as part of the final JSON output. + * Helper method to dispose of an object and remove it from the list of disposables * - * @param output The output returned by running a command. - * @return A string representation of the JSON found in the output. + * @param instance the object to be disposed */ - static getJsonFromOutput(output: string): string { - output = output.substring(output.indexOf('START_OF_TEST_JSON{'), output.lastIndexOf('}END_OF_TEST_JSON') + 1); - // Get rid of the `START_OF_TEST_JSON` and `END_OF_TEST_JSON` to verify that the JSON is valid. - return output.substring(output.indexOf("{"), output.lastIndexOf("}") + 1); + private disposeInstance(instance: vscode.Disposable) { + let index = this.disposables.indexOf(instance); + if (index !== -1) { + this.disposables.splice(index) + } + else { + this.log.debug("Factory instance not null but missing from disposables when configuration changed"); + } + instance.dispose() } /** @@ -119,69 +88,6 @@ export abstract class TestRunner implements vscode.Disposable { // return testSuiteChildren; // } - /** - * Assigns the process to currentChildProcess and handles its output and what happens when it exits. - * - * @param process A process running the tests. - * @return A promise that resolves when the test run completes. - */ - async handleChildProcess(process: childProcess.ChildProcess, context: TestRunContext): Promise { - this.currentChildProcess = process; - let log = this.log.getChildLogger({ label: `ChildProcess(${this.manager.config.frameworkName()})` }) - - process.stderr!.pipe(split2()).on('data', (data) => { - data = data.toString(); - log.trace(data); - if (data.startsWith('Fast Debugger') && this.debugCommandStartedResolver) { - this.debugCommandStartedResolver() - } - }) - - let parsedTests: vscode.TestItem[] = [] - process.stdout!.pipe(split2()).on('data', (data) => { - let getTest = (testId: string): vscode.TestItem => { - testId = this.manager.normaliseTestId(testId) - return this.manager.getOrCreateTestItem(testId) - } - if (data.startsWith('PASSED:')) { - log.debug(`Received test status - PASSED`, data) - context.passed(getTest(data.replace('PASSED: ', ''))) - } else if (data.startsWith('FAILED:')) { - log.debug(`Received test status - FAILED`, data) - let testItem = getTest(data.replace('FAILED: ', '')) - let line = testItem.range?.start?.line ? testItem.range.start.line + 1 : 0 - context.failed(testItem, "", testItem.uri?.fsPath || "", line) - } else if (data.startsWith('RUNNING:')) { - log.debug(`Received test status - RUNNING`, data) - context.started(getTest(data.replace('RUNNING: ', ''))) - } else if (data.startsWith('PENDING:')) { - log.debug(`Received test status - PENDING`, data) - context.skipped(getTest(data.replace('PENDING: ', ''))) - } else if (data.includes('START_OF_TEST_JSON')) { - log.trace("Received test run results", data); - parsedTests = this.parseAndHandleTestOutput(data, context); - } else { - log.trace("Ignoring unrecognised output", data) - } - }); - - await new Promise<{code:number, signal:string}>((resolve, reject) => { - process.once('exit', (code: number, signal: string) => { - log.trace('Child process exited', code, signal) - }); - process.once('close', (code: number, signal: string) => { - log.debug('Child process exited, and all streams closed', code, signal) - resolve({code, signal}); - }); - process.once('error', (err: Error) => { - log.debug('Error event from child process', err.message) - reject(err); - }); - }) - - return parsedTests - }; - /** * Test run handler * @@ -215,8 +121,7 @@ export abstract class TestRunner implements vscode.Disposable { let command: string if (context.request.profile?.label === 'ResolveTests') { command = this.manager.config.getResolveTestsCommand(testsToRun) - let testsRun = await this.runTestFramework(command, context) - this.manager.removeMissingTests(testsRun, testsToRun) + await this.runTestFramework(command, context) } else if (!testsToRun) { log.debug("Running all tests") this.manager.controller.items.forEach((item) => { @@ -244,7 +149,7 @@ export abstract class TestRunner implements vscode.Disposable { } if (debuggerConfig) { log.debug('Debugging tests', request.include?.map(x => x.id)); - await Promise.all([this.startDebugSession(debuggerConfig), this.runTestFramework(command, context)]) + await Promise.all([this.startDebugSession(debuggerConfig, context), this.runTestFramework(command, context)]) } else { log.debug('Running test', request.include?.map(x => x.id)); @@ -264,7 +169,7 @@ export abstract class TestRunner implements vscode.Disposable { } } - private async startDebugSession(debuggerConfig: vscode.DebugConfiguration): Promise { + private async startDebugSession(debuggerConfig: vscode.DebugConfiguration, context: TestRunContext): Promise { let log = this.log.getChildLogger({label: 'startDebugSession'}) if (this.workspace) { @@ -275,7 +180,7 @@ export abstract class TestRunner implements vscode.Disposable { this.log.info('Starting the debug session'); const debugCommandStartedPromise = new Promise((resolve, _) => { - this.debugCommandStartedResolver = resolve + this.debugCommandStartedResolver = () => resolve() }) try { let activeDebugSession: vscode.DebugSession @@ -305,86 +210,16 @@ export abstract class TestRunner implements vscode.Disposable { const debugStopSubscription = vscode.debug.onDidTerminateDebugSession(session => { if (session === activeDebugSession) { log.info('Debug session ended', session.name); - this.killChild(); // terminate the test run + this.killProfileTestRun(context) // terminate the test run debugStopSubscription.dispose(); } }) } catch (err) { log.error('Error starting debug session', err) - this.killChild() + this.killProfileTestRun(context) } } - public parseAndHandleTestOutput(testOutput: string, context?: TestRunContext): vscode.TestItem[] { - let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) - testOutput = TestRunner.getJsonFromOutput(testOutput); - log.trace('Parsing the below JSON:', testOutput); - let testMetadata = JSON.parse(testOutput); - let tests: Array = testMetadata.examples; - - let parsedTests: vscode.TestItem[] = [] - if (tests && tests.length > 0) { - tests.forEach((test: ParsedTest) => { - test.id = this.manager.normaliseTestId(test.id) - - // RSpec provides test ids like "file_name.rb[1:2:3]". - // This uses the digits at the end of the id to create - // an array of numbers representing the location of the - // test in the file. - let test_location_array: Array = test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':'); - let testNumber = test_location_array[test_location_array.length - 1]; - test.file_path = this.manager.normaliseTestId(test.file_path).replace(/\[.*/, '') - let currentFileLabel = test.file_path.split(path.sep).slice(-1)[0] - let pascalCurrentFileLabel = this.snakeToPascalCase(currentFileLabel.replace('_spec.rb', '')); - // If the test doesn't have a name (because it uses the 'it do' syntax), "test #n" - // is appended to the test description to distinguish between separate tests. - let description: string = test.description.startsWith('example at ') ? `${test.full_description}test #${testNumber}` : test.full_description; - - // If the current file label doesn't have a slash in it and it starts with the PascalCase'd - // file name, remove the from the start of the description. This turns, e.g. - // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. - if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { - // Optional check for a space following the PascalCase file name. In some - // cases, e.g. 'FileName#method_name` there's no space after the file name. - let regexString = `${pascalCurrentFileLabel}[ ]?`; - let regex = new RegExp(regexString, "g"); - description = description.replace(regex, ''); - } - test.description = description - let test_location_string: string = test_location_array.join(''); - test.location = parseInt(test_location_string); - - let newTestItem = this.manager.getOrCreateTestItem(test.id) - newTestItem.canResolveChildren = !test.id.endsWith(']') - log.trace('canResolveChildren', test.id, newTestItem.canResolveChildren) - log.trace('label', test.id, description) - newTestItem.label = description - newTestItem.range = new vscode.Range(test.line_number - 1, 0, test.line_number, 0); - parsedTests.push(newTestItem) - log.debug('Parsed test', test) - if(context) { - // Only handle status if actual test run, not dry run - this.handleStatus(test, context); - } - }); - return parsedTests - } - return [] - } - - /** - * Convert a string from snake_case to PascalCase. - * Note that the function will return the input string unchanged if it - * includes a '/'. - * - * @param string The string to convert to PascalCase. - * @return The converted string. - */ - private snakeToPascalCase(string: string): string { - if (string.includes('/')) { return string } - return string.split("_").map(substr => substr.charAt(0).toUpperCase() + substr.slice(1)).join(""); - } - /** * Mark a test node and all its children as being queued for execution */ @@ -408,16 +243,7 @@ export abstract class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - private async runTestFramework (testCommand: string, context: TestRunContext): Promise { - context.token.onCancellationRequested( - () => { - this.log.debug('Cancellation requested') - this.killChild() - }, - this, - this.disposables - ) - + private async runTestFramework (testCommand: string, context: TestRunContext): Promise { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, @@ -425,20 +251,29 @@ export abstract class TestRunner implements vscode.Disposable { }; this.log.debug('Running command', testCommand); + let testProfileKind = context.request.profile!.kind - let testProcess = childProcess.spawn( - testCommand, - spawnArgs - ); + if (this.testProcessMap.get(testProfileKind)) { + this.log.warn('Test run already in progress for profile kind', testProfileKind) + return + } + let testProcess = new FrameworkProcess(this.log, testCommand, spawnArgs, context, this.manager) + this.disposables.push(testProcess) + this.testProcessMap.set(testProfileKind, testProcess); - return await this.handleChildProcess(testProcess, context); + try { + await testProcess.startProcess(this.debugCommandStartedResolver) + } finally { + this.testProcessMap.delete(testProfileKind) + } } - /** - * Handles test state based on the output returned by the test command. - * - * @param test The parsed output from running the test - * @param context Test run context - */ - protected abstract handleStatus(test: ParsedTest, context: TestRunContext): void; + private killProfileTestRun(context: TestRunContext) { + let profileKind = context.request.profile!.kind + let process = this.testProcessMap.get(profileKind) + if (process) { + this.disposeInstance(process) + this.testProcessMap.delete(profileKind) + } + } } diff --git a/src/testStatus.ts b/src/testStatus.ts new file mode 100644 index 0000000..1e01d6d --- /dev/null +++ b/src/testStatus.ts @@ -0,0 +1,18 @@ +import * as vscode from 'vscode' + +export enum Status { + passed, + failed, + errored, + running, + skipped +} + +export class TestStatus { + constructor( + public readonly testItem: vscode.TestItem, + public readonly status: Status, + public readonly duration?: number, + public readonly message?: vscode.TestMessage, + ) {} +} \ No newline at end of file diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index 93dd555..9235330 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -3,6 +3,8 @@ import path from 'path' import { IChildLogger } from '@vscode-logging/logger'; import { Config } from './config'; +export type TestItemCallback = (item: vscode.TestItem) => void + /** * Manages the contents and state of the test suite * @@ -44,17 +46,16 @@ export class TestSuiteManager { /** * Get the {@link vscode.TestItem} for a test ID * @param testId Test ID to lookup + * @param onItemCreated Optional callback to be notified when test items are created * @returns The test item for the ID * @throws if test item could not be found */ - public getOrCreateTestItem(testId: string | vscode.Uri): vscode.TestItem { + public getOrCreateTestItem(testId: string | vscode.Uri, onItemCreated?: TestItemCallback): vscode.TestItem { let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) - testId = this.uriToTestId(testId) - if (testId.startsWith(`.${path.sep}`)) { - testId = testId.substring(2) - } + testId = this.normaliseTestId(this.uriToTestId(testId)) + log.debug('Looking for test', testId) - let parent = this.getOrCreateParent(testId, true) + let parent = this.getOrCreateParent(testId, true, onItemCreated) let testItem = (parent?.children || this.controller.items).get(testId) if (!testItem) { // Create a basic test item with what little info we have to be filled in later @@ -66,7 +67,8 @@ export class TestSuiteManager { testId, label, parent, - !this.locationPattern.test(testId) + onItemCreated, + !this.locationPattern.test(testId), ); } return testItem @@ -89,29 +91,6 @@ export class TestSuiteManager { return testItem } - public removeMissingTests(parsedTests: vscode.TestItem[], requestedTests?: readonly vscode.TestItem[]) { - let log = this.log.getChildLogger({label: `${this.removeMissingTests.name}`}) - - log.debug('Tests to check', JSON.stringify(parsedTests.map(x => x.id))) - - // Files and folders are removed by the filesystem watchers so we only need to clear out single tests - parsedTests = parsedTests.filter(x => !x.canResolveChildren && x.parent) - while (parsedTests.length > 0) { - let parent = parsedTests[0].parent - if (!requestedTests || requestedTests.includes(parent!)) { - // If full suite was resolved we can always replace. If partial suite was resolved, we should - // only replace children if parent was resolved, else we might remove tests that do exist - log.debug('Checking parent', parent?.id) - let parentCollection = parent ? parent.children : this.controller.items - let parentCollectionSize = parentCollection.size - parentCollection.replace(parsedTests.filter(x => x.parent == parent)) - log.debug('Removed tests from parent', parentCollectionSize - parentCollection.size) - } - parsedTests = parsedTests.filter(x => x.parent != parent) - log.debug('Remaining tests to check', parsedTests.length) - } - } - /** * Takes a test ID from the test runner output and normalises it to a consistent format * @@ -161,7 +140,7 @@ export class TestSuiteManager { * @param createIfMissing Create parent test collections if missing * @returns Parent collection of the given test ID */ - private getOrCreateParent(testId: string, createIfMissing: boolean): vscode.TestItem | undefined { + private getOrCreateParent(testId: string, createIfMissing: boolean, onItemCreated?: TestItemCallback): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: `${this.getOrCreateParent.name}(${testId}, createIfMissing: ${createIfMissing})`}) let idSegments = this.splitTestId(testId) let parent: vscode.TestItem | undefined @@ -175,7 +154,9 @@ export class TestSuiteManager { if (!createIfMissing) return undefined child = this.createTestItem( collectionId, - idSegments[i] + idSegments[i], + undefined, + onItemCreated ) } parent = child @@ -197,6 +178,7 @@ export class TestSuiteManager { fileId, fileId.substring(fileId.lastIndexOf(path.sep) + 1), parent, + onItemCreated ) } log.debug('Got TestItem for file from parent collection', fileId) @@ -219,7 +201,8 @@ export class TestSuiteManager { testId: string, label: string, parent?: vscode.TestItem, - canResolveChildren: boolean = true + onItemCreated: TestItemCallback = (_) => {}, + canResolveChildren: boolean = true, ): vscode.TestItem { let log = this.log.getChildLogger({ label: `${this.createTestItem.name}(${testId})` }) let uri = this.testIdToUri(testId) @@ -228,6 +211,7 @@ export class TestSuiteManager { item.canResolveChildren = canResolveChildren; (parent?.children || this.controller.items).add(item); log.debug('Added test', item.id) + onItemCreated(item) return item } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index a19981e..a5d8ee5 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -6,7 +6,7 @@ import { before, beforeEach } from 'mocha'; import { TestLoader } from '../../../src/testLoader'; import { TestSuiteManager } from '../../../src/testSuiteManager'; -import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; +import { TestRunner } from '../../../src/testRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; import { setupMockRequest, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; @@ -17,7 +17,7 @@ suite('Extension Test for Minitest', function() { let testController: vscode.TestController let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] let config: MinitestConfig - let testRunner: MinitestTestRunner; + let testRunner: TestRunner; let testLoader: TestLoader; let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; @@ -78,7 +78,7 @@ suite('Extension Test for Minitest', function() { beforeEach(function () { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new MinitestTestRunner(log, manager, workspaceFolder) + testRunner = new TestRunner(log, manager, true, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); }) @@ -206,7 +206,7 @@ suite('Extension Test for Minitest', function() { before(async function() { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new MinitestTestRunner(log, manager, workspaceFolder) + testRunner = new TestRunner(log, manager, true, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index dc8d1d2..21dc77a 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import { TestLoader } from '../../../src/testLoader'; import { TestSuiteManager } from '../../../src/testSuiteManager'; -import { RspecTestRunner } from '../../../src/rspec/rspecTestRunner'; +import { TestRunner } from '../../../src/testRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; import { setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; @@ -17,7 +17,7 @@ suite('Extension Test for RSpec', function() { let testController: vscode.TestController let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders![0] let config: RspecConfig - let testRunner: RspecTestRunner; + let testRunner: TestRunner; let testLoader: TestLoader; let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; @@ -78,7 +78,7 @@ suite('Extension Test for RSpec', function() { beforeEach(function () { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new RspecTestRunner(log, manager, workspaceFolder) + testRunner = new TestRunner(log, manager, false, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); }) @@ -198,7 +198,7 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new RspecTestRunner(log, manager, workspaceFolder) + testRunner = new TestRunner(log, manager, false, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts new file mode 100644 index 0000000..31226c8 --- /dev/null +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -0,0 +1,255 @@ +import { before, beforeEach } from 'mocha'; +import { instance, mock, when } from 'ts-mockito' +import * as childProcess from 'child_process'; +import * as vscode from 'vscode' +import * as path from 'path' + +import { Config } from "../../../src/config"; +import { TestSuiteManager } from "../../../src/testSuiteManager"; +import { TestRunContext } from '../../../src/testRunContext'; +import { FrameworkProcess } from '../../../src/frameworkProcess'; + +import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; +import { logger } from '../../stubs/logger'; +import { StubTestController } from '../../stubs/stubTestController'; + +const log = logger("info") + +suite('FrameworkProcess', function () { + let manager: TestSuiteManager + let testController: vscode.TestController + let mockContext: TestRunContext + let frameworkProcess: FrameworkProcess + let spawnOptions: childProcess.SpawnOptions = {} + + const config = mock() + + suite('#parseAndHandleTestOutput()', function () { + suite('RSpec output - dry run', function () { + before(function () { + let relativeTestPath = "spec" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + mockContext = mock() + }) + + beforeEach(function () { + testController = new StubTestController(log) + manager = new TestSuiteManager(log, testController, instance(config)) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) + }) + + const expectedTests: TestItemExpectation[] = [ + { + id: "square", + label: "square", + file: path.resolve("spec", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb", + label: "square_spec.rb", + file: path.resolve("spec", "square", "square_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_spec.rb[1:1]", + label: "finds the square of 2", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 3, + }, + { + id: "square/square_spec.rb[1:2]", + label: "finds the square of 3", + file: path.resolve("spec", "square", "square_spec.rb"), + line: 7, + }, + ] + } + ] + }, + { + id: "abs_spec.rb", + label: "abs_spec.rb", + file: path.resolve("spec", "abs_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + file: path.resolve("spec", "abs_spec.rb"), + line: 3, + }, + { + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + file: path.resolve("spec", "abs_spec.rb"), + line: 7, + }, + { + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + file: path.resolve("spec", "abs_spec.rb"), + line: 11, + } + ] + } + ] + const outputJson = { + "version":"3.10.1", + "examples":[ + {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, + {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, + {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} + ], + "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, + "summary_line":"6 examples, 0 failures" + } + const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + + test('parses specs correctly', function () { + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + + suite('Minitest output', function () { + before(function () { + let relativeTestPath = "test" + when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) + when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) + mockContext = mock() + }) + + beforeEach(function () { + testController = new StubTestController(log) + manager = new TestSuiteManager(log, testController, instance(config)) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) + }) + + const expectedTests: TestItemExpectation[] = [ + { + id: "square", + label: "square", + file: path.resolve("test", "square"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb", + label: "square_test.rb", + file: path.resolve("test", "square", "square_test.rb"), + canResolveChildren: true, + children: [ + { + id: "square/square_test.rb[4]", + label: "square 2", + file: path.resolve("test", "square", "square_test.rb"), + line: 3, + }, + { + id: "square/square_test.rb[8]", + label: "square 3", + file: path.resolve("test", "square", "square_test.rb"), + line: 7, + }, + ] + } + ] + }, + { + id: "abs_test.rb", + label: "abs_test.rb", + file: path.resolve("test", "abs_test.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_test.rb[4]", + label: "abs positive", + file: path.resolve("test", "abs_test.rb"), + line: 3, + }, + { + id: "abs_test.rb[8]", + label: "abs 0", + file: path.resolve("test", "abs_test.rb"), + line: 7, + }, + { + id: "abs_test.rb[12]", + label: "abs negative", + file: path.resolve("test", "abs_test.rb"), + line: 11, + } + ] + }, + ] + const outputJson = { + "version":"5.14.4", + "examples":[ + {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, + {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, + {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, + {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, + {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} + ] + } + const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + + test('parses specs correctly', function () { + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + }) + }) + + // suite('getTestSuiteForFile', function() { + // let mockTestRunner: RspecTestRunner + // let testRunner: RspecTestRunner + // let testLoader: TestLoader + // let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] + // let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') + // let id = "abs_spec.rb" + // let abs_spec_item: vscode.TestItem + // let createTestItem = (id: string): vscode.TestItem => { + // return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) + // } + + // this.beforeAll(function () { + // when(config.getRelativeTestDirectory()).thenReturn('spec') + // when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) + // }) + + // this.beforeEach(function () { + // mockTestRunner = mock(RspecTestRunner) + // testRunner = instance(mockTestRunner) + // testController = new StubTestController() + // testSuite = new TestSuite(noop_logger(), testController, instance(config)) + // testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) + // abs_spec_item = createTestItem(id) + // testController.items.add(abs_spec_item) + // }) + + // test('creates test items from output', function () { + // expect(abs_spec_item.children.size).to.eq(0) + + // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + // expect(abs_spec_item.children.size).to.eq(1) + // }) + + // test('removes test items not in output', function () { + // let missing_id = "abs_spec.rb[3:1]" + // let missing_child_item = createTestItem(missing_id) + // abs_spec_item.children.add(missing_child_item) + // expect(abs_spec_item.children.size).to.eq(1) + + // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) + + // expect(abs_spec_item.children.size).to.eq(1) + // expect(abs_spec_item.children.get(missing_id)).to.be.undefined + // expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined + // }) + // }) +}) diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts index 4219c92..f20569d 100644 --- a/test/suite/unitTests/testRunner.test.ts +++ b/test/suite/unitTests/testRunner.test.ts @@ -1,251 +1,199 @@ -//import { expect } from "chai"; -import { before, beforeEach } from 'mocha'; -import { instance, mock, when } from 'ts-mockito' -import * as vscode from 'vscode' -import * as path from 'path' - -import { Config } from "../../../src/config"; -import { TestSuiteManager } from "../../testSuiteManager"; -import { TestRunner } from "../../../src/testRunner"; -import { RspecTestRunner } from "../../../src/rspec/rspecTestRunner"; -import { MinitestTestRunner } from '../../../src/minitest/minitestTestRunner'; -import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; -import { logger } from '../../stubs/logger'; -import { StubTestController } from '../../stubs/stubTestController'; - -const log = logger("off") - -suite('TestRunner', function () { - let manager: TestSuiteManager - let testController: vscode.TestController - let testRunner: TestRunner - - const config = mock() - - suite('#parseAndHandleTestOutput()', function () { - suite('RSpec output', function () { - before(function () { - let relativeTestPath = "spec" - when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) - when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) - }) - - beforeEach(function () { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, instance(config)) - testRunner = new RspecTestRunner(log, manager) - }) - - const expectedTests: TestItemExpectation[] = [ - { - id: "square", - label: "square", - file: path.resolve("spec", "square"), - canResolveChildren: true, - children: [ - { - id: "square/square_spec.rb", - label: "square_spec.rb", - file: path.resolve("spec", "square", "square_spec.rb"), - canResolveChildren: true, - children: [ - { - id: "square/square_spec.rb[1:1]", - label: "finds the square of 2", - file: path.resolve("spec", "square", "square_spec.rb"), - line: 3, - }, - { - id: "square/square_spec.rb[1:2]", - label: "finds the square of 3", - file: path.resolve("spec", "square", "square_spec.rb"), - line: 7, - }, - ] - } - ] - }, - { - id: "abs_spec.rb", - label: "abs_spec.rb", - file: path.resolve("spec", "abs_spec.rb"), - canResolveChildren: true, - children: [ - { - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - file: path.resolve("spec", "abs_spec.rb"), - line: 3, - }, - { - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - file: path.resolve("spec", "abs_spec.rb"), - line: 7, - }, - { - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - file: path.resolve("spec", "abs_spec.rb"), - line: 11, - } - ] - } - ] - const outputJson = { - "version":"3.10.1", - "examples":[ - {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, - {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} - ], - "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, - "summary_line":"6 examples, 0 failures" - } - const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - - test('parses specs correctly', function () { - testRunner.parseAndHandleTestOutput(output) - testItemCollectionMatches(testController.items, expectedTests) - }) - }) - - suite('Minitest output', function () { - before(function () { - let relativeTestPath = "test" - when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) - when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) - }) - - beforeEach(function () { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, instance(config)) - testRunner = new MinitestTestRunner(log, manager) - }) - - const expectedTests: TestItemExpectation[] = [ - { - id: "square", - label: "square", - file: path.resolve("test", "square"), - canResolveChildren: true, - children: [ - { - id: "square/square_test.rb", - label: "square_test.rb", - file: path.resolve("test", "square", "square_test.rb"), - canResolveChildren: true, - children: [ - { - id: "square/square_test.rb[4]", - label: "square 2", - file: path.resolve("test", "square", "square_test.rb"), - line: 3, - }, - { - id: "square/square_test.rb[8]", - label: "square 3", - file: path.resolve("test", "square", "square_test.rb"), - line: 7, - }, - ] - } - ] - }, - { - id: "abs_test.rb", - label: "abs_test.rb", - file: path.resolve("test", "abs_test.rb"), - canResolveChildren: true, - children: [ - { - id: "abs_test.rb[4]", - label: "abs positive", - file: path.resolve("test", "abs_test.rb"), - line: 3, - }, - { - id: "abs_test.rb[8]", - label: "abs 0", - file: path.resolve("test", "abs_test.rb"), - line: 7, - }, - { - id: "abs_test.rb[12]", - label: "abs negative", - file: path.resolve("test", "abs_test.rb"), - line: 11, - } - ] - }, - ] - const outputJson = { - "version":"5.14.4", - "examples":[ - {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, - {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, - {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, - {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, - {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} - ] - } - const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - - test('parses specs correctly', function () { - testRunner.parseAndHandleTestOutput(output) - testItemCollectionMatches(testController.items, expectedTests) - }) - }) - }) - - // suite('getTestSuiteForFile', function() { - // let mockTestRunner: RspecTestRunner - // let testRunner: RspecTestRunner - // let testLoader: TestLoader - // let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] - // let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') - // let id = "abs_spec.rb" - // let abs_spec_item: vscode.TestItem - // let createTestItem = (id: string): vscode.TestItem => { - // return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) - // } - - // this.beforeAll(function () { - // when(config.getRelativeTestDirectory()).thenReturn('spec') - // when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) - // }) - - // this.beforeEach(function () { - // mockTestRunner = mock(RspecTestRunner) - // testRunner = instance(mockTestRunner) - // testController = new StubTestController() - // testSuite = new TestSuite(noop_logger(), testController, instance(config)) - // testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) - // abs_spec_item = createTestItem(id) - // testController.items.add(abs_spec_item) - // }) - - // test('creates test items from output', function () { - // expect(abs_spec_item.children.size).to.eq(0) - - // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - // expect(abs_spec_item.children.size).to.eq(1) - // }) - - // test('removes test items not in output', function () { - // let missing_id = "abs_spec.rb[3:1]" - // let missing_child_item = createTestItem(missing_id) - // abs_spec_item.children.add(missing_child_item) - // expect(abs_spec_item.children.size).to.eq(1) - - // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - // expect(abs_spec_item.children.size).to.eq(1) - // expect(abs_spec_item.children.get(missing_id)).to.be.undefined - // expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined - // }) - // }) -}) +// import { before, beforeEach } from 'mocha'; +// import { instance, mock, when } from 'ts-mockito' +// import * as vscode from 'vscode' +// import * as path from 'path' + +// import { Config } from "../../../src/config"; +// import { TestSuiteManager } from "../../testSuiteManager"; +// import { TestRunner } from "../../../src/testRunner"; +// import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; +// import { logger } from '../../stubs/logger'; +// import { StubTestController } from '../../stubs/stubTestController'; + +// const log = logger("off") + +// suite('TestRunner', function () { +// let manager: TestSuiteManager +// let testController: vscode.TestController +// let testRunner: TestRunner + +// const config = mock() + +// suite('#parseAndHandleTestOutput()', function () { +// suite('RSpec output', function () { +// before(function () { +// let relativeTestPath = "spec" +// when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) +// when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) +// }) + +// beforeEach(function () { +// testController = new StubTestController(log) +// manager = new TestSuiteManager(log, testController, instance(config)) +// testRunner = new TestRunner(log, manager, false) +// }) + +// const expectedTests: TestItemExpectation[] = [ +// { +// id: "square", +// label: "square", +// file: path.resolve("spec", "square"), +// canResolveChildren: true, +// children: [ +// { +// id: "square/square_spec.rb", +// label: "square_spec.rb", +// file: path.resolve("spec", "square", "square_spec.rb"), +// canResolveChildren: true, +// children: [ +// { +// id: "square/square_spec.rb[1:1]", +// label: "finds the square of 2", +// file: path.resolve("spec", "square", "square_spec.rb"), +// line: 3, +// }, +// { +// id: "square/square_spec.rb[1:2]", +// label: "finds the square of 3", +// file: path.resolve("spec", "square", "square_spec.rb"), +// line: 7, +// }, +// ] +// } +// ] +// }, +// { +// id: "abs_spec.rb", +// label: "abs_spec.rb", +// file: path.resolve("spec", "abs_spec.rb"), +// canResolveChildren: true, +// children: [ +// { +// id: "abs_spec.rb[1:1]", +// label: "finds the absolute value of 1", +// file: path.resolve("spec", "abs_spec.rb"), +// line: 3, +// }, +// { +// id: "abs_spec.rb[1:2]", +// label: "finds the absolute value of 0", +// file: path.resolve("spec", "abs_spec.rb"), +// line: 7, +// }, +// { +// id: "abs_spec.rb[1:3]", +// label: "finds the absolute value of -1", +// file: path.resolve("spec", "abs_spec.rb"), +// line: 11, +// } +// ] +// } +// ] +// const outputJson = { +// "version":"3.10.1", +// "examples":[ +// {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, +// {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, +// {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, +// {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, +// {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} +// ], +// "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, +// "summary_line":"6 examples, 0 failures" +// } +// const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + +// test('parses specs correctly', function () { +// testRunner.parseAndHandleTestOutput(output) +// testItemCollectionMatches(testController.items, expectedTests) +// }) +// }) + +// suite('Minitest output', function () { +// before(function () { +// let relativeTestPath = "test" +// when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) +// when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) +// }) + +// beforeEach(function () { +// testController = new StubTestController(log) +// manager = new TestSuiteManager(log, testController, instance(config)) +// testRunner = new TestRunner(log, manager, true) +// }) + +// const expectedTests: TestItemExpectation[] = [ +// { +// id: "square", +// label: "square", +// file: path.resolve("test", "square"), +// canResolveChildren: true, +// children: [ +// { +// id: "square/square_test.rb", +// label: "square_test.rb", +// file: path.resolve("test", "square", "square_test.rb"), +// canResolveChildren: true, +// children: [ +// { +// id: "square/square_test.rb[4]", +// label: "square 2", +// file: path.resolve("test", "square", "square_test.rb"), +// line: 3, +// }, +// { +// id: "square/square_test.rb[8]", +// label: "square 3", +// file: path.resolve("test", "square", "square_test.rb"), +// line: 7, +// }, +// ] +// } +// ] +// }, +// { +// id: "abs_test.rb", +// label: "abs_test.rb", +// file: path.resolve("test", "abs_test.rb"), +// canResolveChildren: true, +// children: [ +// { +// id: "abs_test.rb[4]", +// label: "abs positive", +// file: path.resolve("test", "abs_test.rb"), +// line: 3, +// }, +// { +// id: "abs_test.rb[8]", +// label: "abs 0", +// file: path.resolve("test", "abs_test.rb"), +// line: 7, +// }, +// { +// id: "abs_test.rb[12]", +// label: "abs negative", +// file: path.resolve("test", "abs_test.rb"), +// line: 11, +// } +// ] +// }, +// ] +// const outputJson = { +// "version":"5.14.4", +// "examples":[ +// {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, +// {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, +// {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, +// {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, +// {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} +// ] +// } +// const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` + +// test('parses specs correctly', function () { +// testRunner.parseAndHandleTestOutput(output) +// testItemCollectionMatches(testController.items, expectedTests) +// }) +// }) +// }) +// }) From 575ff559fb7b3cfd7d4cd9378af620de7f36ce59 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 11 Jan 2023 10:17:01 +0000 Subject: [PATCH 064/108] Wire up status events and fix bugs introduced by refactor --- ruby/Rakefile | 1 + ruby/custom_formatter.rb | 41 +++- ruby/rspecs/unit_test.rb | 2 +- ruby/vscode/minitest/reporter.rb | 80 ++++--- src/frameworkProcess.ts | 96 ++++---- src/testRunContext.ts | 37 +-- src/testRunner.ts | 28 +++ .../unitTests/minitest/dryRunOutput.json | 60 +++++ .../unitTests/minitest/testRunOutput.json | 217 ++++++++++++++++++ .../unitTests/rspec/dryRunOutput.json | 68 ++++++ .../unitTests/rspec/testRunOutput.json | 177 ++++++++++++++ test/stubs/stubTestItemCollection.ts | 2 +- test/suite/helpers.ts | 11 +- test/suite/minitest/minitest.test.ts | 29 ++- test/suite/rspec/rspec.test.ts | 27 ++- test/suite/unitTests/frameworkProcess.test.ts | 115 +++------- ...Suite.test.ts => testSuiteManager.test.ts} | 2 +- tsconfig.json | 1 + 18 files changed, 759 insertions(+), 235 deletions(-) create mode 100644 test/fixtures/unitTests/minitest/dryRunOutput.json create mode 100644 test/fixtures/unitTests/minitest/testRunOutput.json create mode 100644 test/fixtures/unitTests/rspec/dryRunOutput.json create mode 100644 test/fixtures/unitTests/rspec/testRunOutput.json rename test/suite/unitTests/{testSuite.test.ts => testSuiteManager.test.ts} (99%) diff --git a/ruby/Rakefile b/ruby/Rakefile index d76c4fb..3291537 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -3,6 +3,7 @@ require "rake/testtask" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:rspectest) do |t| + t.rspec_opts = ['--require /home/tabby/git/vscode-ruby-test-adapter/ruby/custom_formatter.rb', '--format CustomFormatter'] t.pattern = ['rspecs/**/*_spec.rb', 'rspecs/**/*_test.rb'] end diff --git a/ruby/custom_formatter.rb b/ruby/custom_formatter.rb index 4855f1b..5fbe595 100644 --- a/ruby/custom_formatter.rb +++ b/ruby/custom_formatter.rb @@ -4,16 +4,17 @@ require 'rspec/core/formatters/base_formatter' require 'json' +# Formatter to emit RSpec test status information in the required format for the extension class CustomFormatter < RSpec::Core::Formatters::BaseFormatter RSpec::Core::Formatters.register self, - :message, - :dump_summary, - :stop, - :seed, - :close, - :example_passed, - :example_failed, - :example_pending + :message, + :dump_summary, + :stop, + :seed, + :close, + :example_passed, + :example_failed, + :example_pending attr_reader :output_hash @@ -71,8 +72,10 @@ def example_passed(notification) def example_failed(notification) klass = notification.example.exception.class - status = "#{klass.startsWith('RSpec') ? 'FAILED' : 'ERRORED'}: " - output.write "#{status}: #{notification.example.id}\n" + status = exception_is_error?(klass) ? 'ERRORED' : 'FAILED' + exception_message = notification.example.exception.message.gsub(/\s+/, ' ').strip + output.write "#{status}(#{klass.name}:#{exception_message}): " \ + "#{notification.example.id}\n" # This isn't exposed for simplicity, need to figure out how to handle this later. # output.write "#{notification.exception.backtrace.to_json}\n" end @@ -105,7 +108,7 @@ def format_example(example) id: example.id, description: example.description, full_description: example.full_description, - status: example.execution_result.status.to_s, + status: example_status(example), file_path: example.metadata[:file_path], line_number: example.metadata[:line_number], type: example.metadata[:type], @@ -115,9 +118,23 @@ def format_example(example) end def exception_position(backtrace, metadata) - location = backtrace.find { |frame| frame.path.end_with?(metadata[:file]) } + location = backtrace.find { |frame| frame.path.end_with?(metadata[:file_path]) } return metadata[:line_number] unless location location.lineno end + + def example_status(example) + if example.exception && exception_is_error?(example.exception.class) + 'errored' + elsif example.execution_result.status == :pending + 'skipped' + else + example.execution_result.status.to_s + end + end + + def exception_is_error?(exception_class) + !exception_class.to_s.start_with?('RSpec') + end end diff --git a/ruby/rspecs/unit_test.rb b/ruby/rspecs/unit_test.rb index dc9ee79..1a50db6 100644 --- a/ruby/rspecs/unit_test.rb +++ b/ruby/rspecs/unit_test.rb @@ -16,6 +16,6 @@ def square_of(n) end it "finds the square of 3" do - expect(@calculator.square_of(3)).to eq(9) + expect(@calculator.square_of(3)).to eq end end \ No newline at end of file diff --git a/ruby/vscode/minitest/reporter.rb b/ruby/vscode/minitest/reporter.rb index 9f13a87..0990740 100644 --- a/ruby/vscode/minitest/reporter.rb +++ b/ruby/vscode/minitest/reporter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module VSCode module Minitest class Reporter < ::Minitest::Reporter @@ -24,12 +26,9 @@ def record(result) self.count += 1 self.assertions += result.assertions results << result - data = vscode_result(result) - if result.skipped? - io.puts "\nPENDING: #{data[:id]}\n" - else - io.puts "\n#{data[:status].upcase}: #{data[:id]}\n" - end + data = vscode_result(result, false) + + io.puts "#{data[:status]}#{data[:exception]}: #{data[:id]}\n" end def report @@ -39,7 +38,7 @@ def report self.failures = aggregate[::Minitest::Assertion].size self.errors = aggregate[::Minitest::UnexpectedError].size self.skips = aggregate[::Minitest::Skip].size - json = ENV.key?("PRETTY") ? JSON.pretty_generate(vscode_data) : JSON.generate(vscode_data) + json = ENV.key?('PRETTY') ? JSON.pretty_generate(vscode_data) : JSON.generate(vscode_data) io.puts "START_OF_TEST_JSON#{json}END_OF_TEST_JSON" end @@ -59,53 +58,74 @@ def vscode_data }, summary_line: "Total time: #{total_time}, Runs: #{count}, Assertions: #{assertions}, " \ "Failures: #{failures}, Errors: #{errors}, Skips: #{skips}", - examples: results.map { |r| vscode_result(r) } + examples: results.map { |r| vscode_result(r, true) } } end - def vscode_result(r) - base = VSCode::Minitest.tests.find_by(klass: r.klass, method: r.name).dup - if r.skipped? - base[:status] = 'skipped' - base[:pending_message] = r.failure.message - elsif r.passed? - base[:status] = 'passed' + def vscode_result(result, is_report) + base = VSCode::Minitest.tests.find_by(klass: result.klass, method: result.name).dup + + base[:status] = vscode_status(result, is_report) + base[:pending_message] = result.skipped? ? result.failure.message : nil + base[:exception] = vscode_exception(result, base, is_report) + base[:duration] = result.time + base.compact + end + + def vscode_status(result, is_report) + if result.skipped? + status = 'skipped' + elsif result.passed? + status = 'passed' else - e = r.failure.exception - base[:status] = e.class.instance_of?('Minitest::UnexpectedError') ? 'errored' : 'failed' - base[:pending_message] = nil - backtrace = expand_backtrace(e.backtrace) - base[:exception] = { - class: e.class.name, - message: e.message, + e = result.failure.exception + status = e.class.name == ::Minitest::UnexpectedError.name ? 'errored' : 'failed' + end + is_report ? status : status.upcase + end + + def vscode_exception(result, data, is_report) + return if result.passed? || result.skipped? + + err = result.failure.exception + backtrace = expand_backtrace(err.backtrace) + if is_report + { + class: err.class.name, + message: err.message, backtrace: clean_backtrace(backtrace), full_backtrace: backtrace, - position: exception_position(backtrace, base[:full_path]) || base[:line_number] + position: exception_position(backtrace, data[:full_path]) || data[:line_number] } + else + "(#{err.class.name}:#{err.message.tr("\n", ' ').strip})" end - base[:duration] = r.time - base end def expand_backtrace(backtrace) backtrace.map do |line| - parts = line.split(":") + parts = line.split(':') parts[0] = File.expand_path(parts[0], VSCode.project_root) - parts.join(":") + parts.join(':') end end def clean_backtrace(backtrace) backtrace.map do |line| next unless line.start_with?(VSCode.project_root.to_s) - line.gsub(VSCode.project_root.to_s + "/", "") - end.compact + + line[VSCode.project_root.to_s] = '' + line.delete_prefix!('/') + line.delete_prefix!('\\') + line + end end def exception_position(backtrace, file) line = backtrace.find { |frame| frame.start_with?(file) } return unless line - line.split(":")[1].to_i + + line.split(':')[1].to_i end end end diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 8eb832b..31d50c1 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -30,8 +30,9 @@ export class FrameworkProcess implements vscode.Disposable { protected readonly log: IChildLogger; private readonly disposables: vscode.Disposable[] = [] private isDisposed = false; - private readonly testStatusEmitter: vscode.EventEmitter = new vscode.EventEmitter() - private readonly statusPattern = new RegExp(/(?RUNNING|PASSED|FAILED|ERRORED|SKIPPED): (?.*)/) + public readonly testStatusEmitter: vscode.EventEmitter = new vscode.EventEmitter() + private readonly statusPattern = + new RegExp(/(?RUNNING|PASSED|FAILED|ERRORED|SKIPPED)(:?\((:?(?(:?\w*(:?::)?)*)\:)?\s*(?.*)\))?\: (?.*)/) constructor( readonly rootLog: IChildLogger, @@ -67,55 +68,47 @@ export class FrameworkProcess implements vscode.Disposable { } try { + this.log.debug('Starting child process') this.childProcess = childProcess.spawn( this.testCommand, this.spawnArgs ) this.childProcess.stderr!.pipe(split2()).on('data', (data) => { + let log = this.log.getChildLogger({label: 'stderr'}) data = data.toString(); - this.log.trace(data); + log.trace(data); if (data.startsWith('Fast Debugger') && onDebugStarted) { onDebugStarted() } }) - let outputParsedResolver: (value: void | PromiseLike) => void - let outputParsedRejecter: (reason?: any) => void - let outputParsedPromise = new Promise((resolve, reject) => { - outputParsedResolver = resolve - outputParsedRejecter = reject - }) - process.stdout!.pipe(split2()).on('data', (data) => { - this.onDataReceived(data, outputParsedResolver, outputParsedRejecter) + this.childProcess.stdout!.pipe(split2()).on('data', (data) => { + let log = this.log.getChildLogger({label: 'stdout'}) + data = data.toString() + log.trace(data) + this.onDataReceived(data) }); - return (await Promise.all([ - new Promise<{code:number, signal:string}>((resolve, reject) => { - process.once('exit', (code: number, signal: string) => { - this.log.trace('Child process exited', code, signal) - }); - process.once('close', (code: number, signal: string) => { - this.log.debug('Child process exited, and all streams closed', code, signal) - resolve({code, signal}); - }); - process.once('error', (err: Error) => { - this.log.debug('Error event from child process', err.message) - reject(err); - }); - }), - outputParsedPromise - ]))[0] + return await new Promise<{code:number, signal:string}>((resolve, reject) => { + this.childProcess!.once('exit', (code: number, signal: string) => { + this.log.trace('Child process exited', code, signal) + }); + this.childProcess!.once('close', (code: number, signal: string) => { + this.log.debug('Child process exited, and all streams closed', code, signal) + resolve({code, signal}); + }); + this.childProcess!.once('error', (err: Error) => { + this.log.debug('Error event from child process', err.message) + reject(err); + }); + }) } finally { this.dispose() } } - private onDataReceived( - data: string, - outputParsedResolver: (value: void | PromiseLike) => void, - outputParsedRejecter: (reason?: any) => void - ): void { + private onDataReceived(data: string): void { let log = this.log.getChildLogger({label: 'onDataReceived'}) let getTest = (testId: string): vscode.TestItem => { @@ -126,29 +119,42 @@ export class FrameworkProcess implements vscode.Disposable { if (data.includes('START_OF_TEST_JSON')) { log.trace("Received test run results", data); this.parseAndHandleTestOutput(data); - outputParsedResolver() } else { const match = this.statusPattern.exec(data) if (match && match.groups) { + log.trace("Received test status event", data); const id = match.groups['id'] const status = match.groups['status'] - this.testStatusEmitter.fire( - new TestStatus( - getTest(id), - Status[status.toLocaleLowerCase() as keyof typeof Status] - ) - ) + const exception = match.groups['exceptionClass'] + const message = match.groups['exceptionMessage'] + let testItem = getTest(id) + let testMessage: vscode.TestMessage | undefined = undefined + if (message) { + testMessage = new vscode.TestMessage(exception ? `${exception}: ${message}` : message) + // TODO?: get actual exception location, not test location + if (testItem.uri && testItem.range) { + // it should always have a uri, but just to be safe... + testMessage.location = new vscode.Location(testItem.uri, testItem.range) + } else { + log.error('Test missing location details', testItem.id, testItem.uri) + } + } + this.testStatusEmitter.fire(new TestStatus( + testItem, + Status[status.toLocaleLowerCase() as keyof typeof Status], + undefined, // TODO?: get duration info here if possible + testMessage + )) } else { log.trace("Ignoring unrecognised output", data) } } } catch (err) { log.error('Error parsing output', err) - outputParsedRejecter(err) } } - private parseAndHandleTestOutput(testOutput: string): vscode.TestItem[] { + private parseAndHandleTestOutput(testOutput: string): void { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = this.getJsonFromOutput(testOutput); log.trace('Parsing the below JSON:', testOutput); @@ -166,8 +172,7 @@ export class FrameworkProcess implements vscode.Disposable { testItem.canResolveChildren = !test.id.endsWith(']') log.trace('canResolveChildren', test.id, testItem.canResolveChildren) - testItem.description = this.parseDescription(test) - testItem.label = testItem.description + testItem.label = this.parseDescription(test) log.trace('label', test.id, testItem.description) testItem.range = this.parseRange(test) @@ -178,7 +183,9 @@ export class FrameworkProcess implements vscode.Disposable { } log.debug('Parsed test', test) - this.handleStatus(testItem, test) + if (test.status) { + this.handleStatus(testItem, test) + } }); for (const testFile of existingContainers) { /* @@ -191,7 +198,6 @@ export class FrameworkProcess implements vscode.Disposable { testFile.children.replace(parsedTests.filter(x => !x.canResolveChildren && x.parent == testFile)) } } - return [] } private parseDescription(test: ParsedTest): string { diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 67040f7..4887efb 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -52,24 +52,11 @@ export class TestRunContext { */ public errored( test: vscode.TestItem, - message: string, - file: string, - line: number, - duration?: number | undefined + message: vscode.TestMessage, + duration?: number ): void { - let testMessage = new vscode.TestMessage(message) - try { - let testItem = test - testMessage.location = new vscode.Location( - testItem.uri ?? vscode.Uri.file(file), - new vscode.Position(line, 0) - ) - this.log.debug('Errored test', test.id, file, line, duration) - this.log.trace('Error message', message) - this.testRun.errored(testItem, testMessage, duration) - } catch (e: any) { - this.log.error('Failed to report error state for test', test.id, e) - } + this.log.debug('Errored test', test.id, duration, message.message) + this.testRun.errored(test, message, duration) } /** @@ -83,19 +70,11 @@ export class TestRunContext { */ public failed( test: vscode.TestItem, - message: string, - file: string, - line: number, - duration?: number | undefined + message: vscode.TestMessage, + duration?: number ): void { - let testMessage = new vscode.TestMessage(message) - testMessage.location = new vscode.Location( - test.uri ?? vscode.Uri.file(file), - new vscode.Position(line, 0) - ) - this.log.debug('Failed test', test.id, file, line, duration) - this.log.trace('Failure message', message) - this.testRun.failed(test, testMessage, duration) + this.log.debug('Failed test', test.id, duration, message.message) + this.testRun.failed(test, message, duration) } /** diff --git a/src/testRunner.ts b/src/testRunner.ts index 32c7105..85d7963 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -5,6 +5,7 @@ import { __asyncDelegator } from 'tslib'; import { TestRunContext } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; import { FrameworkProcess } from './frameworkProcess'; +import { Status, TestStatus } from './testStatus'; export class TestRunner implements vscode.Disposable { protected debugCommandStartedResolver?: () => void; @@ -261,6 +262,33 @@ export class TestRunner implements vscode.Disposable { this.disposables.push(testProcess) this.testProcessMap.set(testProfileKind, testProcess); + testProcess.testStatusEmitter.event((event: TestStatus) => { + let log = this.log.getChildLogger({label: 'testStatusListener'}) + switch(event.status) { + case Status.skipped: + log.debug('Received test skipped event', event.testItem.id) + context.skipped(event.testItem) + break; + case Status.passed: + log.debug('Received test passed event', event.testItem.id, event.duration) + context.passed(event.testItem, event.duration) + break; + case Status.errored: + log.debug('Received test errored event', event.testItem.id, event.duration, event.message) + context.errored(event.testItem, event.message!, event.duration) + break; + case Status.failed: + log.debug('Received test failed event', event.testItem.id, event.duration, event.message) + context.failed(event.testItem, event.message!, event.duration) + break; + case Status.running: + log.debug('Received test started event', event.testItem.id) + context.started(event.testItem) + break; + default: + log.warn('Unexpected status', event.status) + } + }) try { await testProcess.startProcess(this.debugCommandStartedResolver) } finally { diff --git a/test/fixtures/unitTests/minitest/dryRunOutput.json b/test/fixtures/unitTests/minitest/dryRunOutput.json new file mode 100644 index 0000000..df04ab6 --- /dev/null +++ b/test/fixtures/unitTests/minitest/dryRunOutput.json @@ -0,0 +1,60 @@ +{ + "version": "5.14.4", + "examples": [ + { + "description": "abs positive", + "full_description": "abs positive", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 4, + "klass": "AbsTest", + "method": "test_abs_positive", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[4]" + }, + { + "description": "abs 0", + "full_description": "abs 0", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 8, + "klass": "AbsTest", + "method": "test_abs_0", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[8]" + }, + { + "description": "abs negative", + "full_description": "abs negative", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 12, + "klass": "AbsTest", + "method": "test_abs_negative", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[12]" + }, + { + "description": "square 2", + "full_description": "square 2", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 4, + "klass": "SquareTest", + "method": "test_square_2", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[4]" + }, + { + "description": "square 3", + "full_description": "square 3", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 8, + "klass": "SquareTest", + "method": "test_square_3", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[8]" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/unitTests/minitest/testRunOutput.json b/test/fixtures/unitTests/minitest/testRunOutput.json new file mode 100644 index 0000000..f0009862d --- /dev/null +++ b/test/fixtures/unitTests/minitest/testRunOutput.json @@ -0,0 +1,217 @@ +{ + "version": "5.14.4", + "summary": { + "duration": 0.05, + "example_count": 3, + "failure_count": 1, + "pending_count": 1, + "errors_outside_of_examples_count": 1 + }, + "summary_line": "Total time: 0.05, Runs: 5, Assertions: 3, Failures: 1, Errors: 1, Skips: 1", + "examples": [ + { + "description": "abs 0", + "full_description": "abs 0", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 8, + "klass": "AbsTest", + "method": "test_abs_0", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[8]", + "status": "failed", + "pending_message": null, + "exception": { + "class": "Minitest::UnexpectedError", + "message": "RuntimeError: Abs for zero is not supported\n /home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/lib/abs.rb:7:in `apply'\n /home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb:9:in `test_abs_0'", + "backtrace": [ + "lib/abs.rb:7:in `apply'", + "test/abs_test.rb:9:in `test_abs_0'" + ], + "full_backtrace": [ + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/lib/abs.rb:7:in `apply'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb:9:in `test_abs_0'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:98:in `block (3 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:195:in `capture_exceptions'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:95:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:272:in `time_it'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:94:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:211:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:93:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:1029:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:341:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:328:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:354:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:326:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:40:in `block in run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `each'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest.rb:36:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode.rake:14:in `block (3 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `block in execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:219:in `block in invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `synchronize'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:188:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:160:in `invoke_task'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block (2 levels) in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:125:in `run_with_threads'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:110:in `top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:83:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:186:in `standard_exception_handling'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:80:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/exe/rake:27:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/base.rb:476:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:46:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/friendly_errors.rb:123:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:34:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 9 + }, + "duration": 0.00010449799447087571 + }, + { + "description": "abs negative", + "full_description": "abs negative", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 12, + "klass": "AbsTest", + "method": "test_abs_negative", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[12]", + "status": "skipped", + "pending_message": "Not implemented yet", + "duration": 0.00031753199436934665 + }, + { + "description": "abs positive", + "full_description": "abs positive", + "file_path": "./test/abs_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb", + "line_number": 4, + "klass": "AbsTest", + "method": "test_abs_positive", + "runnable": "AbsTest", + "id": "./test/abs_test.rb[4]", + "status": "passed", + "duration": 6.183300138218328e-05 + }, + { + "description": "square 2", + "full_description": "square 2", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 4, + "klass": "SquareTest", + "method": "test_square_2", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[4]", + "status": "passed", + "duration": 8.17399995867163e-05 + }, + { + "description": "square 3", + "full_description": "square 3", + "file_path": "./test/square/square_test.rb", + "full_path": "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb", + "line_number": 8, + "klass": "SquareTest", + "method": "test_square_3", + "runnable": "SquareTest", + "id": "./test/square/square_test.rb[8]", + "status": "failed", + "pending_message": null, + "exception": { + "class": "Minitest::Assertion", + "message": "Expected: 9\n Actual: 6", + "backtrace": [ + "test/square/square_test.rb:9:in `test_square_3'" + ], + "full_backtrace": [ + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/assertions.rb:183:in `assert'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/assertions.rb:218:in `assert_equal'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb:9:in `test_square_3'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:98:in `block (3 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:195:in `capture_exceptions'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:95:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:272:in `time_it'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:94:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:211:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest/test.rb:93:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:1029:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:341:in `run_one_method'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:328:in `block (2 levels) in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:327:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:367:in `on_signal'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:354:in `with_info_handler'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.14.4/lib/minitest.rb:326:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:40:in `block in run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `each'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest/runner.rb:39:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode/minitest.rb:36:in `run'", + "/home/tabby/git/vscode-ruby-test-adapter/ruby/vscode.rake:14:in `block (3 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `block in execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:281:in `execute'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:219:in `block in invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `synchronize'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:199:in `invoke_with_call_chain'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/task.rb:188:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:160:in `invoke_task'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block (2 levels) in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `each'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:116:in `block in top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:125:in `run_with_threads'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:110:in `top_level'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:83:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:186:in `standard_exception_handling'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/lib/rake/application.rb:80:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rake-13.0.3/exe/rake:27:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rake:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/base.rb:476:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:46:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/lib/bundler/friendly_errors.rb:123:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.1.4/exe/bundle:34:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 9 + }, + "duration": 0.0003147919996990822 + } + ] +} diff --git a/test/fixtures/unitTests/rspec/dryRunOutput.json b/test/fixtures/unitTests/rspec/dryRunOutput.json new file mode 100644 index 0000000..bc7286b --- /dev/null +++ b/test/fixtures/unitTests/rspec/dryRunOutput.json @@ -0,0 +1,68 @@ +{ + "version": "3.10.1", + "examples": [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 2.9555e-05 + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 1.1982e-05 + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": null, + "duration": 1.0685e-05 + }, + { + "id": "./spec/square/square_spec.rb[1:1]", + "description": "finds the square of 2", + "full_description": "Square finds the square of 2", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 1.7352e-05 + }, + { + "id": "./spec/square/square_spec.rb[1:2]", + "description": "finds the square of 3", + "full_description": "Square finds the square of 3", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 1.2148e-05 + } + ], + "summary": { + "duration": 0.002985345, + "example_count": 5, + "failure_count": 0, + "pending_count": 0, + "errors_outside_of_examples_count": 0 + }, + "summary_line": "5 examples, 0 failures" +} \ No newline at end of file diff --git a/test/fixtures/unitTests/rspec/testRunOutput.json b/test/fixtures/unitTests/rspec/testRunOutput.json new file mode 100644 index 0000000..e6a9345 --- /dev/null +++ b/test/fixtures/unitTests/rspec/testRunOutput.json @@ -0,0 +1,177 @@ +{ + "version": "3.10.1", + "examples": [ + { + "id": "./spec/abs_spec.rb[1:1]", + "description": "finds the absolute value of 1", + "full_description": "Abs finds the absolute value of 1", + "status": "passed", + "file_path": "./spec/abs_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 0.001351962 + }, + { + "id": "./spec/abs_spec.rb[1:2]", + "description": "finds the absolute value of 0", + "full_description": "Abs finds the absolute value of 0", + "status": "failed", + "file_path": "./spec/abs_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 0.00035244, + "exception": { + "class": "RuntimeError", + "message": "Abs for zero is not supported", + "backtrace": [ + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/lib/abs.rb:7:in `apply'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/spec/abs_spec.rb:9:in `block (2 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `instance_exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `block in with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `block in with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:259:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:644:in `block in run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:606:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2067:in `with_suite_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:116:in `block in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/reporter.rb:74:in `report'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:115:in `run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:89:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:71:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:45:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec:4:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:494:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:49:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/friendly_errors.rb:130:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:37:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 8 + } + }, + { + "id": "./spec/abs_spec.rb[1:3]", + "description": "finds the absolute value of -1", + "full_description": "Abs finds the absolute value of -1", + "status": "pending", + "file_path": "./spec/abs_spec.rb", + "line_number": 12, + "type": null, + "pending_message": "No reason given", + "duration": 0.000438123 + }, + { + "id": "./spec/square/square_spec.rb[1:1]", + "description": "finds the square of 2", + "full_description": "Square finds the square of 2", + "status": "passed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 4, + "type": null, + "pending_message": null, + "duration": 0.000480215 + }, + { + "id": "./spec/square/square_spec.rb[1:2]", + "description": "finds the square of 3", + "full_description": "Square finds the square of 3", + "status": "failed", + "file_path": "./spec/square/square_spec.rb", + "line_number": 8, + "type": null, + "pending_message": null, + "duration": 0.086019708, + "exception": { + "class": "RSpec::Expectations::ExpectationNotMetError", + "message": "\nexpected: 9\n got: 6\n\n(compared using ==)\n", + "backtrace": [ + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:102:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:111:in `notify_failure'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/fail_with.rb:35:in `fail_with'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:38:in `handle_failure'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:56:in `block in handle_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:27:in`with_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/handler.rb:48:in `handle_matcher'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.10.1/lib/rspec/expectations/expectation_target.rb:65:in `to'", + "/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/rspec/spec/square/square_spec.rb:9:in `block (2 levels) in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `instance_exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:262:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `block in with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `block in with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `block in run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/hooks.rb:486:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:465:in `with_around_example_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:508:in `with_around_and_singleton_context_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example.rb:259:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:644:in `block in run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:640:in `run_examples'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/example_group.rb:606:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `map'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/configuration.rb:2067:in `with_suite_hooks'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:116:in `block in run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/reporter.rb:74:in `report'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:115:in `run_specs'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:89:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:71:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib/rspec/core/runner.rb:45:in `invoke'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec:4:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/rspec:25:in `'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:63:in `kernel_load'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli/exec.rb:28:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:494:in `exec'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:30:in `dispatch'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/cli.rb:24:in `start'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:49:in `block in '", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/lib/bundler/friendly_errors.rb:130:in `with_friendly_errors'", + "/home/tabby/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/bundler-2.2.13/exe/bundle:37:in `'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `load'", + "/home/tabby/.rbenv/versions/3.1.0/bin/bundle:25:in `
'" + ], + "position": 8 + } + } + ], + "summary": { + "duration": 0.094338312, + "example_count": 5, + "failure_count": 2, + "pending_count": 1, + "errors_outside_of_examples_count": 0 + }, + "summary_line": "5 examples, 2 failures, 1 pending" +} \ No newline at end of file diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index 9062d43..491cf45 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -14,7 +14,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } replace(items: readonly vscode.TestItem[]): void { - //this.log.debug(`Replacing all tests`, JSON.stringify(Object.keys(this.testIds)), JSON.stringify(items.map(x => x.id))) + this.log.debug(`Replacing all tests`, JSON.stringify(Object.keys(this.testIds)), JSON.stringify(items.map(x => x.id))) this.testIds = {} items.forEach(item => { this.testIds[item.id] = item diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index 6f9f0e3..cbc97bd 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -25,7 +25,7 @@ export type TestItemExpectation = { * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values */ export type TestFailureExpectation = { - message?: string, + message?: RegExp, actualOutput?: string, expectedOutput?: string, line?: number, @@ -97,7 +97,7 @@ export function testItemCollectionMatches( if(!expectedItem) { expect.fail(`${testItem.id} not found in expected items`) } - testItemMatches(testItem, expectedItem) + testItemMatches(testItem, expectedItem, parent ? `collection(${parent.id})` : undefined) }) } @@ -111,12 +111,13 @@ export function verifyFailure( let failure = captor.byCallIndex(index) let testItem = failure[0] let failureDetails = failure[1] - let messagePrefix = message ? `${message} - ${testItem.id}` : testItem.id + let messagePrefix = `${testItem.id} (call: ${index})` + messagePrefix = message ? `${message} - ${messagePrefix}` : messagePrefix testItemMatches(testItem, expectedTestItem) if (expectation.message) { - expect(failureDetails.message).to.contain(expectation.message, `${messagePrefix}: message`) + expect(failureDetails.message).to.match(expectation.message, `${messagePrefix}: message`) } else { - expect(failureDetails.message).to.eq('') + expect(failureDetails.message).to.eq('', "Expected not to receive an exception backtrace") } expect(failureDetails.actualOutput).to.eq(expectation.actualOutput, `${messagePrefix}: actualOutput`) expect(failureDetails.expectedOutput).to.eq(expectation.expectedOutput, `${messagePrefix}: expectedOutput`) diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index a5d8ee5..a083af6 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -22,7 +22,7 @@ suite('Extension Test for Minitest', function() { let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; - const log = logger("info"); + const log = logger("debug"); let expectedPath = (file: string): string => { return path.resolve( @@ -223,8 +223,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) @@ -237,8 +237,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) @@ -252,8 +252,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(7) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) }) @@ -268,7 +268,7 @@ suite('Extension Test for Minitest', function() { status: "failed", expectedTest: square_3_expectation, failureExpectation: { - message: "Expected: 9\n Actual: 6\n", + message: /Expected: 9\s*Actual: 6/, line: 8, } }, @@ -280,7 +280,7 @@ suite('Extension Test for Minitest', function() { status: "errored", expectedTest: abs_zero_expectation, failureExpectation: { - message: "RuntimeError: Abs for zero is not supported", + message: /RuntimeError: Abs for zero is not supported/, line: 8, } }, @@ -305,7 +305,7 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "failed": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {...failureExpectation!, line: failureExpectation!.line! - 1}) verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(2) @@ -313,16 +313,15 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "errored": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, {...failureExpectation!, line: failureExpectation!.line! - 1}) + verifyFailure(1, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(0) break; case "skipped": testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) - testItemMatches(testStateCaptors(mockTestRun).skippedArg(1), expectedTest) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(0) verify(mockTestRun.errored(anything(), anything(), anything())).times(0) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 21dc77a..afbede7 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -215,8 +215,8 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.enqueued(anything())).times(0) verify(mockTestRun.started(anything())).times(8) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) @@ -229,8 +229,8 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.enqueued(anything())).times(0) verify(mockTestRun.started(anything())).times(8) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) @@ -244,8 +244,8 @@ suite('Extension Test for RSpec', function() { // One less 'started' than the other tests as it doesn't include the 'square' folder verify(mockTestRun.started(anything())).times(7) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(3) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) }) }) @@ -260,7 +260,7 @@ suite('Extension Test for RSpec', function() { status: "failed", expectedTest: square_3_expectation, failureExpectation: { - message: "RSpec::Expectations::ExpectationNotMetError:\n expected: 9\n got: 6\n", + message: /RSpec::Expectations::ExpectationNotMetError:\s*expected: 9\s*got: 6/, line: 8, } }, @@ -272,7 +272,7 @@ suite('Extension Test for RSpec', function() { status: "errored", expectedTest: abs_zero_expectation, failureExpectation: { - message: "RuntimeError:\nAbs for zero is not supported", + message: /RuntimeError:\s*Abs for zero is not supported/, line: 8, } }, @@ -297,7 +297,7 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "failed": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {...failureExpectation!, line: expectedTest.line}) verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(2) @@ -305,16 +305,15 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "errored": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {line: failureExpectation!.line}) - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, {...failureExpectation!, line: expectedTest.line}) + verifyFailure(1, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(1) - verify(mockTestRun.errored(anything(), anything(), anything())).times(1) + verify(mockTestRun.failed(anything(), anything(), anything())).times(0) + verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(0) break; case "skipped": testItemMatches(testStateCaptors(mockTestRun).skippedArg(0), expectedTest) - testItemMatches(testStateCaptors(mockTestRun).skippedArg(1), expectedTest) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(0) verify(mockTestRun.errored(anything(), anything(), anything())).times(0) diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 31226c8..6e37778 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -13,7 +13,14 @@ import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; import { logger } from '../../stubs/logger'; import { StubTestController } from '../../stubs/stubTestController'; -const log = logger("info") +// JSON Fixtures +import rspecDryRunOutput from '../../fixtures/unitTests/rspec/dryRunOutput.json' +import rspecTestRunOutput from '../../fixtures/unitTests/rspec/testRunOutput.json' +import minitestDryRunOutput from '../../fixtures/unitTests/minitest/dryRunOutput.json' +import minitestTestRunOutput from '../../fixtures/unitTests/minitest/testRunOutput.json' + +const log = logger("debug") +const cancellationTokenSoure = new vscode.CancellationTokenSource() suite('FrameworkProcess', function () { let manager: TestSuiteManager @@ -24,13 +31,17 @@ suite('FrameworkProcess', function () { const config = mock() + before(function () { + mockContext = mock() + when(mockContext.cancellationToken).thenReturn(cancellationTokenSoure.token) + }) + suite('#parseAndHandleTestOutput()', function () { - suite('RSpec output - dry run', function () { + suite('RSpec output', function () { before(function () { let relativeTestPath = "spec" when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) - mockContext = mock() }) beforeEach(function () { @@ -95,32 +106,25 @@ suite('FrameworkProcess', function () { ] } ] - const outputJson = { - "version":"3.10.1", - "examples":[ - {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, - {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, - {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} - ], - "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, - "summary_line":"6 examples, 0 failures" - } - const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - - test('parses specs correctly', function () { + + test('parses dry run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(rspecDryRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + + test('parses test run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(rspecTestRunOutput)}END_OF_TEST_JSON` frameworkProcess['parseAndHandleTestOutput'](output) testItemCollectionMatches(testController.items, expectedTests) }) }) - suite('Minitest output', function () { + suite('Minitest output - dry run', function () { before(function () { let relativeTestPath = "test" when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) - mockContext = mock() }) beforeEach(function () { @@ -185,71 +189,18 @@ suite('FrameworkProcess', function () { ] }, ] - const outputJson = { - "version":"5.14.4", - "examples":[ - {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, - {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, - {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, - {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, - {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} - ] - } - const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - - test('parses specs correctly', function () { + + test('parses dry run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(minitestDryRunOutput)}END_OF_TEST_JSON` + frameworkProcess['parseAndHandleTestOutput'](output) + testItemCollectionMatches(testController.items, expectedTests) + }) + + test('parses test run output correctly', function () { + const output = `START_OF_TEST_JSON${JSON.stringify(minitestTestRunOutput)}END_OF_TEST_JSON` frameworkProcess['parseAndHandleTestOutput'](output) testItemCollectionMatches(testController.items, expectedTests) }) }) }) - - // suite('getTestSuiteForFile', function() { - // let mockTestRunner: RspecTestRunner - // let testRunner: RspecTestRunner - // let testLoader: TestLoader - // let parsedTests = [{"id":"abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"abs_spec.rb","line_number":4,"type":null,"pending_message":null,"location":11}] - // let expectedPath = path.resolve('test', 'fixtures', 'rspec', 'spec') - // let id = "abs_spec.rb" - // let abs_spec_item: vscode.TestItem - // let createTestItem = (id: string): vscode.TestItem => { - // return testController.createTestItem(id, id, vscode.Uri.file(path.resolve(expectedPath, id))) - // } - - // this.beforeAll(function () { - // when(config.getRelativeTestDirectory()).thenReturn('spec') - // when(config.getAbsoluteTestDirectory()).thenReturn(expectedPath) - // }) - - // this.beforeEach(function () { - // mockTestRunner = mock(RspecTestRunner) - // testRunner = instance(mockTestRunner) - // testController = new StubTestController() - // testSuite = new TestSuite(noop_logger(), testController, instance(config)) - // testLoader = new TestLoader(noop_logger(), testController, testRunner, config, testSuite) - // abs_spec_item = createTestItem(id) - // testController.items.add(abs_spec_item) - // }) - - // test('creates test items from output', function () { - // expect(abs_spec_item.children.size).to.eq(0) - - // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - // expect(abs_spec_item.children.size).to.eq(1) - // }) - - // test('removes test items not in output', function () { - // let missing_id = "abs_spec.rb[3:1]" - // let missing_child_item = createTestItem(missing_id) - // abs_spec_item.children.add(missing_child_item) - // expect(abs_spec_item.children.size).to.eq(1) - - // testLoader.getTestSuiteForFile(parsedTests, abs_spec_item) - - // expect(abs_spec_item.children.size).to.eq(1) - // expect(abs_spec_item.children.get(missing_id)).to.be.undefined - // expect(abs_spec_item.children.get("abs_spec.rb[1:1]")).to.not.be.undefined - // }) - // }) }) diff --git a/test/suite/unitTests/testSuite.test.ts b/test/suite/unitTests/testSuiteManager.test.ts similarity index 99% rename from test/suite/unitTests/testSuite.test.ts rename to test/suite/unitTests/testSuiteManager.test.ts index 36aa155..ae1bba8 100644 --- a/test/suite/unitTests/testSuite.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import path from 'path' import { Config } from '../../../src/config'; -import { TestSuiteManager } from '../../testSuiteManager'; +import { TestSuiteManager } from '../../../src/testSuiteManager'; import { StubTestController } from '../../stubs/stubTestController'; import { NOOP_LOGGER } from '../../stubs/logger'; import { testUriMatches } from '../helpers'; diff --git a/tsconfig.json b/tsconfig.json index 2635ede..3dc6d9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "removeComments": true, "skipLibCheck": true, "esModuleInterop": true, + "resolveJsonModule": true, "baseUrl": "./", } } From 36dbe4917d32c825763513e6c74a37c192cd069a Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 11 Jan 2023 23:43:49 +0000 Subject: [PATCH 065/108] Disable GPU acceleration in vscode test instance Doesn't work in Xvfb and just slows down tests --- test/runFrameworkTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 5d036c3..0df798d 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -43,7 +43,7 @@ async function runTestSuite(vscodeExecutablePath: string, suite: string) { extensionDevelopmentPath, extensionTestsPath: testsPath, extensionTestsEnv: { "TEST_SUITE": suite }, - launchArgs: [fixturesPath], + launchArgs: ['--disable-gpu', fixturesPath], vscodeExecutablePath: vscodeExecutablePath }).catch((error: any) => { console.error(error); From abeefd6dbcbafcf2f9ab0ae3113a1152d6dbd982 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 12 Jan 2023 00:07:23 +0000 Subject: [PATCH 066/108] Update vsce dependency to @vscode/vsce-2.16.0 --- package-lock.json | 710 +++++++++++++++++++++++++--------------------- package.json | 4 +- 2 files changed, 389 insertions(+), 325 deletions(-) diff --git a/package-lock.json b/package-lock.json index cff3b21..a23b3e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,13 +21,13 @@ "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", "@vscode/test-electron": "^2.1.2", + "@vscode/vsce": "^2.16.0", "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", "ts-mockito": "^2.6.1", - "typescript": "^4.7.4", - "vsce": "^2.6.7" + "typescript": "^4.7.4" }, "engines": { "vscode": "^1.69.0" @@ -151,6 +151,155 @@ "node": ">=8.9.3" } }, + "node_modules/@vscode/vsce": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.16.0.tgz", + "integrity": "sha512-BhJ0zO7UxShLFBZM6jwOLt1ZVoqQ4r5Lj/kHNeYp0ICPXhz/erqBSMQnHkRgkjn2L/bh+TYFGkZyguhu/SKsjw==", + "dev": true, + "dependencies": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.4.23", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -271,7 +420,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/big-integer": { "version": "1.6.51", @@ -309,6 +459,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -320,6 +471,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -387,6 +539,7 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -580,7 +733,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/cliui": { "version": "7.0.4", @@ -745,6 +899,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -772,6 +927,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "optional": true, "engines": { "node": ">=4.0.0" } @@ -781,6 +937,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, + "optional": true, "engines": { "node": ">=8" } @@ -874,6 +1031,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "optional": true, "dependencies": { "once": "^1.4.0" } @@ -924,6 +1082,7 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, + "optional": true, "engines": { "node": ">=6" } @@ -993,7 +1152,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "optional": true }, "node_modules/fs-extra": { "version": "9.1.0", @@ -1140,7 +1300,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "dev": true, + "optional": true }, "node_modules/glob": { "version": "8.0.3", @@ -1317,7 +1478,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/inflight": { "version": "1.0.6", @@ -1338,7 +1500,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "optional": true }, "node_modules/is-arrayish": { "version": "0.3.2", @@ -1468,6 +1631,7 @@ "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, + "optional": true, "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" @@ -1619,6 +1783,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "optional": true, "engines": { "node": ">=10" }, @@ -1660,7 +1825,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "node_modules/mocha": { "version": "9.2.2", @@ -1797,13 +1963,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/node-abi": { "version": "3.24.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.24.0.tgz", "integrity": "sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==", "dev": true, + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -1816,6 +1984,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, + "optional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -1830,7 +1999,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true + "dev": true, + "optional": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -1993,6 +2163,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -2024,6 +2195,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2058,6 +2230,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -2073,6 +2246,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -2276,7 +2450,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -2297,6 +2472,7 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -2481,6 +2657,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -2493,6 +2670,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -2509,6 +2687,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2597,6 +2776,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -2686,197 +2866,50 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/vsce": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.10.0.tgz", - "integrity": "sha512-b+wB3XMapEi368g64klSM6uylllZdNutseqbNY+tUoHYSy6g2NwnlWuAGKDQTYc0IqfDUjUFRQBpPgA89Q+Fyw==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" + "isexe": "^2.0.0" }, "bin": { - "vsce": "vsce" + "node-which": "bin/node-which" }, "engines": { - "node": ">= 14" + "node": ">= 8" } }, - "node_modules/vsce/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "node_modules/winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", "dependencies": { - "color-convert": "^1.9.0" + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" }, "engines": { - "node": ">=4" + "node": ">= 6.4.0" } }, - "node_modules/vsce/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "node_modules/winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/vsce/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/vsce/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", - "dependencies": { - "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", - "is-stream": "^2.0.0", - "logform": "^2.2.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", - "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", - "dependencies": { - "readable-stream": "^2.3.6", - "triple-beam": "^1.2.0" - }, - "engines": { - "node": ">= 6.4.0" + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" } }, "node_modules/winston/node_modules/readable-stream": { @@ -3155,6 +3188,125 @@ "unzipper": "^0.10.11" } }, + "@vscode/vsce": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.16.0.tgz", + "integrity": "sha512-BhJ0zO7UxShLFBZM6jwOLt1ZVoqQ4r5Lj/kHNeYp0ICPXhz/erqBSMQnHkRgkjn2L/bh+TYFGkZyguhu/SKsjw==", + "dev": true, + "requires": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "keytar": "^7.7.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.4.23", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3237,7 +3389,8 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "dev": true, + "optional": true }, "big-integer": { "version": "1.6.51", @@ -3266,6 +3419,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3277,6 +3431,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3326,6 +3481,7 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, + "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -3465,7 +3621,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "optional": true }, "cliui": { "version": "7.0.4", @@ -3601,6 +3758,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "optional": true, "requires": { "mimic-response": "^3.1.0" } @@ -3618,13 +3776,15 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "dev": true, + "optional": true }, "detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "dev": true + "dev": true, + "optional": true }, "diff": { "version": "5.0.0", @@ -3694,6 +3854,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "optional": true, "requires": { "once": "^1.4.0" } @@ -3728,7 +3889,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true + "dev": true, + "optional": true }, "fast-safe-stringify": { "version": "2.0.7", @@ -3783,7 +3945,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "optional": true }, "fs-extra": { "version": "9.1.0", @@ -3898,7 +4061,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "dev": true, + "optional": true }, "glob": { "version": "8.0.3", @@ -4017,7 +4181,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "dev": true, + "optional": true }, "inflight": { "version": "1.0.6", @@ -4038,7 +4203,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "optional": true }, "is-arrayish": { "version": "0.3.2", @@ -4132,6 +4298,7 @@ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, + "optional": true, "requires": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" @@ -4254,7 +4421,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true + "dev": true, + "optional": true }, "minimatch": { "version": "4.2.1", @@ -4284,7 +4452,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "mocha": { "version": "9.2.2", @@ -4393,13 +4562,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "node-abi": { "version": "3.24.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.24.0.tgz", "integrity": "sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==", "dev": true, + "optional": true, "requires": { "semver": "^7.3.5" }, @@ -4409,6 +4580,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4419,7 +4591,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true + "dev": true, + "optional": true }, "normalize-path": { "version": "3.0.0", @@ -4540,6 +4713,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, + "optional": true, "requires": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -4565,6 +4739,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -4593,6 +4768,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -4604,7 +4780,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true + "dev": true, + "optional": true } } }, @@ -4750,13 +4927,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true + "dev": true, + "optional": true }, "simple-get": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "optional": true, "requires": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -4908,6 +5087,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -4920,6 +5100,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -4933,6 +5114,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5005,6 +5187,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -5078,125 +5261,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "vsce": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.10.0.tgz", - "integrity": "sha512-b+wB3XMapEi368g64klSM6uylllZdNutseqbNY+tUoHYSy6g2NwnlWuAGKDQTYc0IqfDUjUFRQBpPgA89Q+Fyw==", - "dev": true, - "requires": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index d3882e8..9ceaace 100644 --- a/package.json +++ b/package.json @@ -56,13 +56,13 @@ "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", "@vscode/test-electron": "^2.1.2", + "@vscode/vsce": "^2.16.0", "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", "ts-mockito": "^2.6.1", - "typescript": "^4.7.4", - "vsce": "^2.6.7" + "typescript": "^4.7.4" }, "engines": { "vscode": "^1.69.0" From 24b2c12008a4dbd34789001692488ac1eaf5e1b3 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 12 Jan 2023 00:29:50 +0000 Subject: [PATCH 067/108] Add RSpec deeply nested contexts spec and update label expectations for files I realised that the files ought to have the top level describe/class name as their label, not the filename. Also wanted to be sure RSpec contexts are correctly handled, as I suspect they're not yet --- test/fixtures/rspec/spec/contexts_spec.rb | 31 +++++ test/suite/minitest/minitest.test.ts | 4 +- test/suite/rspec/rspec.test.ts | 107 +++++++++++++++++- test/suite/unitTests/frameworkProcess.test.ts | 10 +- test/suite/unitTests/testSuiteManager.test.ts | 25 ++++ 5 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/rspec/spec/contexts_spec.rb diff --git a/test/fixtures/rspec/spec/contexts_spec.rb b/test/fixtures/rspec/spec/contexts_spec.rb new file mode 100644 index 0000000..fed02b1 --- /dev/null +++ b/test/fixtures/rspec/spec/contexts_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +describe 'Contexts' do + context 'when' do + context 'there' do + context 'are' do + context 'many' do + context 'levels' do + context 'of' do + context 'nested' do + context 'contexts' do + it "doesn't break the extension" do + expect('Hello text explorer!').to be_a(string) + end + end + end + end + end + end + + context 'fewer levels of nested contexts' do + it "still doesn't break the extension" do + expect('Hello again text explorer!').to be_a(string) + end + end + end + end + end +end diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index a083af6..719c3f6 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -101,7 +101,7 @@ suite('Extension Test for Minitest', function() { { file: expectedPath("abs_test.rb"), id: "abs_test.rb", - label: "abs_test.rb", + label: "Abs", canResolveChildren: true, children: [] }, @@ -114,7 +114,7 @@ suite('Extension Test for Minitest', function() { { file: expectedPath("square/square_test.rb"), id: "square/square_test.rb", - label: "square_test.rb", + label: "Square", canResolveChildren: true, children: [] }, diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index afbede7..58ab43f 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -63,6 +63,18 @@ suite('Extension Test for RSpec', function() { label: "finds the square of 3", line: 7, } + let contexts_many_expectation = { + file: expectedPath('contexts_spec.rb'), + id: 'contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]', + label: "doesn't break the extension", + line: 13, + } + let contexts_fewer_expectation = { + file: expectedPath('contexts_spec.rb'), + id: 'contexts_spec.rb[1:1:1:1:2:1]', + label: "still doesn't break the extension", + line: 23, + } before(function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') @@ -88,6 +100,8 @@ suite('Extension Test for RSpec', function() { testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) let absSpecItem = createTest("abs_spec.rb") testController.items.add(absSpecItem) + let contextsSpecItem = createTest("contexts_spec.rb") + testController.items.add(contextsSpecItem) let subfolderItem = createTest("square") testController.items.add(subfolderItem) subfolderItem.children.add(createTest("square/square_spec.rb", "square_spec.rb")) @@ -101,6 +115,12 @@ suite('Extension Test for RSpec', function() { label: "abs_spec.rb", children: [] }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + label: "contexts_spec.rb", + children: [] + }, { file: expectedPath("square"), id: "square", @@ -127,16 +147,25 @@ suite('Extension Test for RSpec', function() { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", label: "abs_spec.rb", + canResolveChildren: true, children: [ abs_positive_expectation, abs_zero_expectation, abs_negative_expectation ] }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + label: "contexts_spec.rb", + canResolveChildren: true, + children: [] + }, { file: expectedPath("square"), id: "square", label: "square", + canResolveChildren: true, children: [ { file: expectedPath("square/square_spec.rb"), @@ -161,7 +190,7 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", - label: "abs_spec.rb", + label: "Abs", canResolveChildren: true, children: [ abs_positive_expectation, @@ -169,6 +198,80 @@ suite('Extension Test for RSpec', function() { abs_negative_expectation ] }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb", + label: "Contexts", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1]", + label: "when", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1]", + label: "there", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1]", + label: "many", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1]", + label: "levels", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1]", + label: "of", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1]", + label: "nested", + canResolveChildren: true, + children: [ + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + label: "contexts", + canResolveChildren: true, + children: [ + contexts_many_expectation, + ] + }, + ] + }, + ] + }, + ] + }, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:2]", + label: "fewer levels of nested contexts", + canResolveChildren: true, + children: [ + contexts_fewer_expectation, + ] + } + ] + } + ] + } + ] + } + ] + }, { file: expectedPath("square"), id: "square", @@ -178,7 +281,7 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("square/square_spec.rb"), id: "square/square_spec.rb", - label: "square_spec.rb", + label: "Square", canResolveChildren: true, children: [ square_2_expectation, diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 6e37778..2aa6860 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -19,7 +19,7 @@ import rspecTestRunOutput from '../../fixtures/unitTests/rspec/testRunOutput.jso import minitestDryRunOutput from '../../fixtures/unitTests/minitest/dryRunOutput.json' import minitestTestRunOutput from '../../fixtures/unitTests/minitest/testRunOutput.json' -const log = logger("debug") +const log = logger("trace") const cancellationTokenSoure = new vscode.CancellationTokenSource() suite('FrameworkProcess', function () { @@ -59,7 +59,7 @@ suite('FrameworkProcess', function () { children: [ { id: "square/square_spec.rb", - label: "square_spec.rb", + label: "Square", file: path.resolve("spec", "square", "square_spec.rb"), canResolveChildren: true, children: [ @@ -81,7 +81,7 @@ suite('FrameworkProcess', function () { }, { id: "abs_spec.rb", - label: "abs_spec.rb", + label: "Abs", file: path.resolve("spec", "abs_spec.rb"), canResolveChildren: true, children: [ @@ -142,7 +142,7 @@ suite('FrameworkProcess', function () { children: [ { id: "square/square_test.rb", - label: "square_test.rb", + label: "Square", file: path.resolve("test", "square", "square_test.rb"), canResolveChildren: true, children: [ @@ -164,7 +164,7 @@ suite('FrameworkProcess', function () { }, { id: "abs_test.rb", - label: "abs_test.rb", + label: "Abs", file: path.resolve("test", "abs_test.rb"), canResolveChildren: true, children: [ diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts index ae1bba8..2b4d735 100644 --- a/test/suite/unitTests/testSuiteManager.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -161,6 +161,31 @@ suite('TestSuite', function () { testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), id)) }) + test('creates intermediate items if ID implies contexts', function () { + let fileId = 'not-found' + let contextId = `${fileId}[1:1]` + let testId = `${fileId}[1:1:1]` + + + let testItem = manager.getOrCreateTestItem(testId) + expect(testItem).to.not.be.undefined + expect(testItem?.id).to.eq(testId) + expect(testItem.canResolveChildren).to.eq(false) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), testId)) + + let contextItem = manager.getTestItem(contextId) + expect(contextItem).to.not.be.undefined + expect(contextItem?.id).to.eq(contextId) + expect(contextItem?.canResolveChildren).to.eq(true) + testUriMatches(testItem, contextItem?.uri?.fsPath) + + let fileItem = manager.getTestItem(fileId) + expect(fileItem).to.not.be.undefined + expect(fileItem?.id).to.eq(fileId) + expect(fileItem?.canResolveChildren).to.eq(true) + testUriMatches(testItem, fileItem?.uri?.fsPath) + }) + test('creates item and parent if parent of nested file is not found', function () { let id = `folder${path.sep}not-found` let testItem = manager.getOrCreateTestItem(id) From 72b9edf2984cd36427827d8caf591aeb13784c44 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Thu, 12 Jan 2023 21:03:35 +0000 Subject: [PATCH 068/108] Fix parent ID handling for RSpec contexts --- src/testSuiteManager.ts | 202 ++++++++---------- test/suite/unitTests/testSuiteManager.test.ts | 73 ++++++- 2 files changed, 155 insertions(+), 120 deletions(-) diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index 9235330..df7aef5 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -12,7 +12,6 @@ export type TestItemCallback = (item: vscode.TestItem) => void */ export class TestSuiteManager { private readonly log: IChildLogger; - private readonly locationPattern = /\[[0-9]*(?::[0-9]*)*\]$/ constructor( readonly rootLog: IChildLogger, @@ -26,20 +25,17 @@ export class TestSuiteManager { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.uriToTestId(testId) log.debug('Deleting test', testId) - let parent = this.getOrCreateParent(testId, false) - let collection: vscode.TestItemCollection | undefined - if (!parent) { - log.debug('Parent is controller') - collection = this.controller.items - } else { - log.debug('Parent', parent.id) - collection = parent.children + let testItem = this.getTestItem(testId) + if (!testItem) { + log.error('No test item found with given ID', testId) + return } + let collection = testItem.parent ? testItem.parent.children : this.controller.items if (collection) { collection.delete(testId); log.debug('Removed test', testId) } else { - log.error('Collection not found') + log.error('Parent collection not found') } } @@ -52,26 +48,7 @@ export class TestSuiteManager { */ public getOrCreateTestItem(testId: string | vscode.Uri, onItemCreated?: TestItemCallback): vscode.TestItem { let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) - testId = this.normaliseTestId(this.uriToTestId(testId)) - - log.debug('Looking for test', testId) - let parent = this.getOrCreateParent(testId, true, onItemCreated) - let testItem = (parent?.children || this.controller.items).get(testId) - if (!testItem) { - // Create a basic test item with what little info we have to be filled in later - let label = testId.substring(testId.lastIndexOf(path.sep) + 1) - if (this.locationPattern.test(testId)) { - label = this.getPlaceholderLabelForSingleTest(testId) - } - testItem = this.createTestItem( - testId, - label, - parent, - onItemCreated, - !this.locationPattern.test(testId), - ); - } - return testItem + return this.getTestItemInternal(log, testId, true, onItemCreated)! } /** @@ -81,14 +58,7 @@ export class TestSuiteManager { */ public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: 'getTestItem'}) - testId = this.uriToTestId(testId) - let parent = this.getOrCreateParent(testId, false) - let testItem = (parent?.children || this.controller.items).get(testId) - if (!testItem) { - log.debug("Couldn't find test with ID", testId) - return undefined - } - return testItem + return this.getTestItemInternal(log, testId, false) } /** @@ -134,60 +104,6 @@ export class TestSuiteManager { return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId.replace(/\[.*\]/, ''))) } - /** - * Searches the collection of tests for the TestItemCollection that contains the given test ID - * @param testId ID of the test to get the parent collection of - * @param createIfMissing Create parent test collections if missing - * @returns Parent collection of the given test ID - */ - private getOrCreateParent(testId: string, createIfMissing: boolean, onItemCreated?: TestItemCallback): vscode.TestItem | undefined { - let log = this.log.getChildLogger({label: `${this.getOrCreateParent.name}(${testId}, createIfMissing: ${createIfMissing})`}) - let idSegments = this.splitTestId(testId) - let parent: vscode.TestItem | undefined - - // Walk through test folders to find the collection containing our test file - for (let i = 0; i < idSegments.length - 1; i++) { - let collectionId = this.getPartialId(idSegments, i) - log.debug('Getting parent collection', collectionId) - let child = this.controller.items.get(collectionId) - if (!child) { - if (!createIfMissing) return undefined - child = this.createTestItem( - collectionId, - idSegments[i], - undefined, - onItemCreated - ) - } - parent = child - } - - // TODO: This might not handle nested describe/context/etc blocks? - if (this.locationPattern.test(testId)) { - // Test item is a test within a file - // Need to make sure we strip locations from file id to get final collection - let fileId = testId.replace(this.locationPattern, '') - if (fileId.startsWith(path.sep)) { - fileId = fileId.substring(1) - } - let child = (parent?.children || this.controller.items).get(fileId) - if (!child) { - log.debug('TestItem for file not in parent collection', fileId) - if (!createIfMissing) return undefined - child = this.createTestItem( - fileId, - fileId.substring(fileId.lastIndexOf(path.sep) + 1), - parent, - onItemCreated - ) - } - log.debug('Got TestItem for file from parent collection', fileId) - parent = child - } - // else test item is the file so return the file's parent - return parent - } - /** * Creates a TestItem and adds it to a TestItemCollection * @param collection @@ -216,34 +132,98 @@ export class TestSuiteManager { } /** - * Builds the testId of a parent folder from the parts of a child ID up to the given depth - * @param idSegments array of segments of a test ID (e.g. ['foo', 'bar', 'bat.rb'] would be the segments for the test item 'foo/bar/bat.rb') - * @param depth number of segments to use to build the ID - * @returns test ID of a parent folder + * Splits a test ID into an array of all parent IDs to reach the given ID from the test tree root + * @param testId test ID to split + * @returns array of test IDs */ - private getPartialId(idSegments: string[], depth: number): string { - return (depth == 0) - ? idSegments[0] - : idSegments.slice(0, depth + 1).join(path.sep) - } - - /** - * Splits a test ID into segments by path separator - * @param testId - * @returns - */ - private splitTestId(testId: string): string[] { - let log = this.log.getChildLogger({label: `splitTestId(${testId})`}) + private getParentIdsFromId(testId: string): string[] { + let log = this.log.getChildLogger({label: `${this.getParentIdsFromId.name}(${testId})`}) testId = this.normaliseTestId(testId) + + // Split path segments let idSegments = testId.split(path.sep) log.debug('id segments', idSegments) if (idSegments[0] === "") { idSegments.splice(0, 1) } + log.trace('ID segments split by path', idSegments) + for (let i = 1; i < idSegments.length - 1; i++) { + let currentSegment = idSegments[i] + let precedingSegments = idSegments.slice(0, i + 1) + log.trace(`segment: ${currentSegment}. preceding segments`, precedingSegments) + idSegments[i] = path.join(...precedingSegments) + } + log.trace('ID segments joined with preceding segments', idSegments) + + // Split location + const match = idSegments.at(-1)?.match(/(?[^\[]*)(?:\[(?[0-9:]+)\])?/) + if (match && match.groups) { + // Get file ID (with path to it if there is one) + let fileId = match.groups["fileId"] + log.trace('Filename', fileId) + if (idSegments.length > 1) { + fileId = path.join(idSegments.at(-2)!, fileId) + log.trace('Filename with path', fileId) + } + // Add file ID to array + idSegments.splice(-1, 1, fileId) + log.trace('ID segments with file ID inserted', idSegments) + + if (match.groups["location"]) { + let locations = match.groups["location"].split(':') + log.trace('ID location segments', locations) + if (locations.length == 1) { + // Insert ID for minitest location + let contextId = `${fileId}[${locations[0]}]` + idSegments.push(contextId) + } else { + // Insert IDs for each nested RSpec context if there are any + for (let i = 1; i < locations.length; i++) { + let contextId = `${fileId}[${locations.slice(0, i + 1).join(':')}]` + idSegments.push(contextId) + } + } + log.trace('ID segments with location IDs appended', idSegments) + } + } return idSegments } - private getPlaceholderLabelForSingleTest(testId: string): string { - return `Awaiting test details... (location: ${this.locationPattern.exec(testId)})` + private getTestItemInternal( + log: IChildLogger, + testId: string | vscode.Uri, + createIfMissing: boolean, + onItemCreated?: TestItemCallback + ): vscode.TestItem | undefined { + testId = this.normaliseTestId(this.uriToTestId(testId)) + + log.debug('Looking for test', testId) + let parentIds = this.getParentIdsFromId(testId) + let item: vscode.TestItem | undefined = undefined + let itemCollection: vscode.TestItemCollection = this.controller.items + + // Walk through test folders to find the collection containing our test file, + // creating parent items as needed + for (const id of parentIds) { + log.debug('Getting item from parent collection', id, item?.id || 'controller') + let child = itemCollection.get(id) + if (!child) { + if (createIfMissing) { + child = this.createTestItem( + id, + id, // Temporary label + item, + onItemCreated, + !(id == testId) // Only the test ID will be a test case. All parents need this set to true + ) + } else { + return undefined + } + } + item = child + itemCollection = child.children + } + + return item } } diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts index 2b4d735..cfed424 100644 --- a/test/suite/unitTests/testSuiteManager.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -7,9 +7,11 @@ import path from 'path' import { Config } from '../../../src/config'; import { TestSuiteManager } from '../../../src/testSuiteManager'; import { StubTestController } from '../../stubs/stubTestController'; -import { NOOP_LOGGER } from '../../stubs/logger'; +import { logger } from '../../stubs/logger'; import { testUriMatches } from '../helpers'; +const log = logger("trace") + suite('TestSuite', function () { let mockConfig: Config = mock(); const config: Config = instance(mockConfig) @@ -23,8 +25,8 @@ suite('TestSuite', function () { }); beforeEach(function () { - controller = new StubTestController(NOOP_LOGGER) - manager = new TestSuiteManager(NOOP_LOGGER, controller, instance(mockConfig)) + controller = new StubTestController(log) + manager = new TestSuiteManager(log, controller, instance(mockConfig)) }); suite('#normaliseTestId()', function () { @@ -151,7 +153,7 @@ suite('TestSuite', function () { }) test('creates item if nested ID is not found', function () { - let id = `folder${path.sep}not-found` + let id = path.join('folder', 'not-found.rb') let folderItem = controller.createTestItem('folder', 'folder') controller.items.add(folderItem) @@ -171,7 +173,7 @@ suite('TestSuite', function () { expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq(testId) expect(testItem.canResolveChildren).to.eq(false) - testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), testId)) + testUriMatches(testItem, path.resolve(config.getAbsoluteTestDirectory(), fileId)) let contextItem = manager.getTestItem(contextId) expect(contextItem).to.not.be.undefined @@ -187,7 +189,7 @@ suite('TestSuite', function () { }) test('creates item and parent if parent of nested file is not found', function () { - let id = `folder${path.sep}not-found` + let id = path.join('folder', 'not-found.rb') let testItem = manager.getOrCreateTestItem(id) expect(testItem).to.not.be.undefined expect(testItem?.id).to.eq(id) @@ -199,7 +201,7 @@ suite('TestSuite', function () { }) suite('creates full item tree for specs within files', function () { - let fileId = `folder${path.sep}not-found.rb` + let fileId = path.join('folder', 'not-found.rb') for (const {suite, location} of [ {suite: 'minitest', location: '[4]'}, @@ -208,8 +210,8 @@ suite('TestSuite', function () { test(suite, function() { let id = `${fileId}${location}` let testItem = manager.getOrCreateTestItem(id) - expect(testItem.id).to.eq(id) - expect(testItem.parent?.id).to.eq(fileId) + expect(testItem.id).to.eq(id, 'testItem ID') + expect(testItem.parent?.id).to.eq(fileId, 'file ID') let folderItem = manager.getTestItem('folder') let fileItem = manager.getTestItem(fileId) @@ -222,4 +224,57 @@ suite('TestSuite', function () { } }) }) + + suite('#getParentIdsFromId', function() { + test('does nothing for a top level file ID', function() { + let id = "some-file.rb" + expect(manager["getParentIdsFromId"](id)).to.eql([id]) + }) + + test('splits file path segments from folder structure', function() { + let id = path.join("path", "to", "some-file.rb") + expect(manager["getParentIdsFromId"](id)).to.eql( + ['path', path.join("path", "to"), id] + ) + }); + + test('splits RSpec location array', function() { + let id = "some-file.rb[1:2:3]" + expect(manager["getParentIdsFromId"](id)).to.eql( + ['some-file.rb', 'some-file.rb[1:2]', 'some-file.rb[1:2:3]'] + ) + }); + + test('splits file path segments and RSpec location array', function() { + let id = path.join('path', 'to', 'some-file.rb[1:2:3]') + expect(manager['getParentIdsFromId'](id)).to.eql( + [ + 'path', + path.join('path', 'to'), + path.join('path', 'to', 'some-file.rb'), + path.join('path', 'to', 'some-file.rb[1:2]'), + path.join('path', 'to', 'some-file.rb[1:2:3]') + ] + ) + }); + + test('splits Minitest location array', function() { + let id = "some-file.rb[7]" + expect(manager["getParentIdsFromId"](id)).to.eql( + ['some-file.rb', 'some-file.rb[7]'] + ) + }); + + test('splits file path segments and Minitest location array', function() { + let id = path.join('path', 'to', 'some-file.rb[7]') + expect(manager["getParentIdsFromId"](id)).to.eql( + [ + 'path', + path.join('path', 'to'), + path.join('path', 'to', 'some-file.rb'), + path.join('path', 'to', 'some-file.rb[7]') + ] + ) + }); + }) }); From dc62d9f7c12a2149d2ebc361d891ebad464feaa9 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 02:23:41 +0000 Subject: [PATCH 069/108] Fix more small issues * Fix bugs around canResolveChildren flag on testItems * Enable test started notifications from RSpec and handle them * Clean up TestSuiteManager a bit (remove uri params on some methods) * Fix error in contexts_spec * Revert expectation changes around labels (will fix these in a later PR as it will be a pain without significant changes to the data sent from the test frameworks and this PR is big enough already) * Get tests all green --- ruby/custom_formatter.rb | 12 +- src/frameworkProcess.ts | 2 +- src/testFactory.ts | 1 - src/testLoader.ts | 27 ++- src/testRunner.ts | 9 +- src/testSuiteManager.ts | 42 ++-- test/fixtures/rspec/spec/contexts_spec.rb | 6 +- .../unitTests/rspec/dryRunOutput.json | 38 +++- .../unitTests/rspec/testRunOutput.json | 26 ++- test/stubs/stubTestController.ts | 6 +- test/suite/helpers.ts | 1 + test/suite/minitest/minitest.test.ts | 12 +- test/suite/rspec/rspec.test.ts | 91 +++++--- test/suite/unitTests/frameworkProcess.test.ts | 184 +++++++++++++--- test/suite/unitTests/testRunner.test.ts | 199 ------------------ test/suite/unitTests/testSuiteManager.test.ts | 65 +++++- 16 files changed, 388 insertions(+), 333 deletions(-) delete mode 100644 test/suite/unitTests/testRunner.test.ts diff --git a/ruby/custom_formatter.rb b/ruby/custom_formatter.rb index 5fbe595..3cbf4f4 100644 --- a/ruby/custom_formatter.rb +++ b/ruby/custom_formatter.rb @@ -12,9 +12,11 @@ class CustomFormatter < RSpec::Core::Formatters::BaseFormatter :stop, :seed, :close, + # :example_group_started, :example_passed, :example_failed, - :example_pending + :example_pending, + :example_started attr_reader :output_hash @@ -84,6 +86,14 @@ def example_pending(notification) output.write "SKIPPED: #{notification.example.id}\n" end + def example_started(notification) + output.write "RUNNING: #{notification.example.id}\n" + end + + # def example_group_started(notification) + # output.write "RUNNING: #{notification.group.id}\n" + # end + private # Properties of example: diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 31d50c1..9564b9c 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -220,7 +220,7 @@ export class FrameworkProcess implements vscode.Disposable { // If the current file label doesn't have a slash in it and it starts with the PascalCase'd // file name, remove the from the start of the description. This turns, e.g. // `ExternalAccount Validations blah blah blah' into 'Validations blah blah blah'. - if (!pascalCurrentFileLabel.includes('/') && description.startsWith(pascalCurrentFileLabel)) { + if (!pascalCurrentFileLabel.includes(path.sep) && description.startsWith(pascalCurrentFileLabel)) { // Optional check for a space following the PascalCase file name. In some // cases, e.g. 'FileName#method_name` there's no space after the file name. let regexString = `${pascalCurrentFileLabel}[ ]?`; diff --git a/src/testFactory.ts b/src/testFactory.ts index 6508948..f9913f9 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -60,7 +60,6 @@ export class TestFactory implements vscode.Disposable { this.runner = new TestRunner( this.log, this.manager, - this.framework == "minitest", this.workspace, ) this.disposables.push(this.runner); diff --git a/src/testLoader.ts b/src/testLoader.ts index 438a8c9..3035f31 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import path from 'path' import { IChildLogger } from '@vscode-logging/logger'; import { TestSuiteManager } from './testSuiteManager'; import { LoaderQueue } from './loaderQueue'; @@ -53,13 +54,13 @@ export class TestLoader implements vscode.Disposable { watcher.onDidCreate(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidCreate watcher'}) watcherLog.debug('File created', uri.fsPath) - this.manager.getOrCreateTestItem(uri) + this.manager.getOrCreateTestItem(this.uriToTestId(uri)) }) // When files change, reload them watcher.onDidChange(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidChange watcher'}) watcherLog.debug('File changed, reloading tests', uri.fsPath) - let testItem = this.manager.getTestItem(uri) + let testItem = this.manager.getTestItem(this.uriToTestId(uri)) if (!testItem) { watcherLog.error('Unable to find test item for file', uri) } else { @@ -70,7 +71,7 @@ export class TestLoader implements vscode.Disposable { watcher.onDidDelete(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidDelete watcher'}) watcherLog.debug('File deleted', uri.fsPath) - this.manager.deleteTestItem(uri) + this.manager.deleteTestItem(this.uriToTestId(uri)) }); return watcher; @@ -99,7 +100,7 @@ export class TestLoader implements vscode.Disposable { for (const file of await vscode.workspace.findFiles(pattern)) { log.debug('Found file, creating TestItem', file) // Enqueue the file to load tests from it - resolveFilesPromises.push(this.resolveQueue.enqueue(this.manager.getOrCreateTestItem(file))) + resolveFilesPromises.push(this.resolveQueue.enqueue(this.manager.getOrCreateTestItem(this.uriToTestId(file)))) } // TODO - skip if filewatcher for this pattern exists and dispose filewatchers for patterns no longer in config @@ -149,4 +150,22 @@ export class TestLoader implements vscode.Disposable { } }) } + + /** + * Converts a test URI into a test ID + * @param uri URI of test + * @returns test ID + */ + private uriToTestId(uri: string | vscode.Uri): string { + let log = this.log.getChildLogger({label: `uriToTestId(${uri})`}) + if (typeof uri === "string") { + log.debug("uri is string. Returning unchanged") + return uri + } + let fullTestDirPath = this.manager.config.getAbsoluteTestDirectory() + log.debug('Full path to test dir', fullTestDirPath) + let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') + log.debug('Stripped URI', strippedUri) + return strippedUri + } } diff --git a/src/testRunner.ts b/src/testRunner.ts index 85d7963..885ad3c 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -21,7 +21,6 @@ export class TestRunner implements vscode.Disposable { constructor( readonly rootLog: IChildLogger, protected manager: TestSuiteManager, - private readonly canNotifyOnStartingTests: boolean, protected workspace?: vscode.WorkspaceFolder, ) { this.log = rootLog.getChildLogger({label: "TestRunner"}) @@ -225,12 +224,8 @@ export class TestRunner implements vscode.Disposable { * Mark a test node and all its children as being queued for execution */ private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { - if (this.canNotifyOnStartingTests) { - // Tests will be marked as started as the runner gets to them - context.enqueued(test); - } else { - context.started(test); - } + // Tests will be marked as started as the runner gets to them + context.enqueued(test); if (test.children && test.children.size > 0) { test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) } diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index df7aef5..67021c4 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -21,9 +21,9 @@ export class TestSuiteManager { this.log = rootLog.getChildLogger({label: 'TestSuite'}); } - public deleteTestItem(testId: string | vscode.Uri) { + public deleteTestItem(testId: string) { let log = this.log.getChildLogger({label: 'deleteTestItem'}) - testId = this.uriToTestId(testId) + testId = this.normaliseTestId(testId) log.debug('Deleting test', testId) let testItem = this.getTestItem(testId) if (!testItem) { @@ -46,7 +46,7 @@ export class TestSuiteManager { * @returns The test item for the ID * @throws if test item could not be found */ - public getOrCreateTestItem(testId: string | vscode.Uri, onItemCreated?: TestItemCallback): vscode.TestItem { + public getOrCreateTestItem(testId: string, onItemCreated?: TestItemCallback): vscode.TestItem { let log = this.log.getChildLogger({label: 'getOrCreateTestItem'}) return this.getTestItemInternal(log, testId, true, onItemCreated)! } @@ -56,7 +56,7 @@ export class TestSuiteManager { * @param testId ID of the TestItem to get * @returns TestItem if found, else undefined */ - public getTestItem(testId: string | vscode.Uri): vscode.TestItem | undefined { + public getTestItem(testId: string): vscode.TestItem | undefined { let log = this.log.getChildLogger({label: 'getTestItem'}) return this.getTestItemInternal(log, testId, false) } @@ -82,24 +82,6 @@ export class TestSuiteManager { return testId } - /** - * Converts a test URI into a test ID - * @param uri URI of test - * @returns test ID - */ - private uriToTestId(uri: string | vscode.Uri): string { - let log = this.log.getChildLogger({label: `uriToTestId(${uri})`}) - if (typeof uri === "string") { - log.debug("uri is string. Returning unchanged") - return uri - } - let fullTestDirPath = this.config.getAbsoluteTestDirectory() - log.debug('Full path to test dir', fullTestDirPath) - let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') - log.debug('Stripped URI', strippedUri) - return strippedUri - } - private testIdToUri(testId: string): vscode.Uri { return vscode.Uri.file(path.resolve(this.config.getAbsoluteTestDirectory(), testId.replace(/\[.*\]/, ''))) } @@ -191,11 +173,11 @@ export class TestSuiteManager { private getTestItemInternal( log: IChildLogger, - testId: string | vscode.Uri, + testId: string, createIfMissing: boolean, onItemCreated?: TestItemCallback ): vscode.TestItem | undefined { - testId = this.normaliseTestId(this.uriToTestId(testId)) + testId = this.normaliseTestId(testId) log.debug('Looking for test', testId) let parentIds = this.getParentIdsFromId(testId) @@ -211,10 +193,10 @@ export class TestSuiteManager { if (createIfMissing) { child = this.createTestItem( id, - id, // Temporary label + id.substring(id.lastIndexOf(path.sep) + 1), // Temporary label item, onItemCreated, - !(id == testId) // Only the test ID will be a test case. All parents need this set to true + this.canResolveChildren(id, testId) // Only the test ID will be a test case. All parents need this set to true ) } else { return undefined @@ -226,4 +208,12 @@ export class TestSuiteManager { return item } + + private canResolveChildren(itemId: string, testId: string): boolean { + if (itemId.endsWith(']')) { + return itemId !== testId + } else { + return true + } + } } diff --git a/test/fixtures/rspec/spec/contexts_spec.rb b/test/fixtures/rspec/spec/contexts_spec.rb index fed02b1..0d12b34 100644 --- a/test/fixtures/rspec/spec/contexts_spec.rb +++ b/test/fixtures/rspec/spec/contexts_spec.rb @@ -12,7 +12,7 @@ context 'nested' do context 'contexts' do it "doesn't break the extension" do - expect('Hello text explorer!').to be_a(string) + expect('Hello text explorer!').to be_a(String) end end end @@ -21,8 +21,8 @@ end context 'fewer levels of nested contexts' do - it "still doesn't break the extension" do - expect('Hello again text explorer!').to be_a(string) + it do + expect('Hello again text explorer!').to be_a(String) end end end diff --git a/test/fixtures/unitTests/rspec/dryRunOutput.json b/test/fixtures/unitTests/rspec/dryRunOutput.json index bc7286b..1313e4b 100644 --- a/test/fixtures/unitTests/rspec/dryRunOutput.json +++ b/test/fixtures/unitTests/rspec/dryRunOutput.json @@ -10,7 +10,7 @@ "line_number": 4, "type": null, "pending_message": null, - "duration": 2.9555e-05 + "duration": 3.0758e-05 }, { "id": "./spec/abs_spec.rb[1:2]", @@ -21,7 +21,7 @@ "line_number": 8, "type": null, "pending_message": null, - "duration": 1.1982e-05 + "duration": 1.2055e-05 }, { "id": "./spec/abs_spec.rb[1:3]", @@ -32,7 +32,29 @@ "line_number": 12, "type": null, "pending_message": null, - "duration": 1.0685e-05 + "duration": 1.0444e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + "description": "doesn't break the extension", + "full_description": "Contexts when there are many levels of nested contexts doesn't break the extension", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 14, + "type": null, + "pending_message": null, + "duration": 3.7666e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:2:1]", + "description": "example at ./spec/contexts_spec.rb:24", + "full_description": "Contexts when there are fewer levels of nested contexts ", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 24, + "type": null, + "pending_message": null, + "duration": 4.024e-05 }, { "id": "./spec/square/square_spec.rb[1:1]", @@ -43,7 +65,7 @@ "line_number": 4, "type": null, "pending_message": null, - "duration": 1.7352e-05 + "duration": 4.3333e-05 }, { "id": "./spec/square/square_spec.rb[1:2]", @@ -54,15 +76,15 @@ "line_number": 8, "type": null, "pending_message": null, - "duration": 1.2148e-05 + "duration": 0.000158812 } ], "summary": { - "duration": 0.002985345, - "example_count": 5, + "duration": 0.007978328, + "example_count": 7, "failure_count": 0, "pending_count": 0, "errors_outside_of_examples_count": 0 }, - "summary_line": "5 examples, 0 failures" + "summary_line": "7 examples, 0 failures" } \ No newline at end of file diff --git a/test/fixtures/unitTests/rspec/testRunOutput.json b/test/fixtures/unitTests/rspec/testRunOutput.json index e6a9345..bf35dc8 100644 --- a/test/fixtures/unitTests/rspec/testRunOutput.json +++ b/test/fixtures/unitTests/rspec/testRunOutput.json @@ -85,6 +85,28 @@ "pending_message": "No reason given", "duration": 0.000438123 }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + "description": "doesn't break the extension", + "full_description": "Contexts when there are many levels of nested contexts doesn't break the extension", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 14, + "type": null, + "pending_message": null, + "duration": 3.7666e-05 + }, + { + "id": "./spec/contexts_spec.rb[1:1:1:1:2:1]", + "description": "example at ./spec/contexts_spec.rb:24", + "full_description": "Contexts when there are fewer levels of nested contexts ", + "status": "passed", + "file_path": "./spec/contexts_spec.rb", + "line_number": 24, + "type": null, + "pending_message": null, + "duration": 4.024e-05 + }, { "id": "./spec/square/square_spec.rb[1:1]", "description": "finds the square of 2", @@ -168,10 +190,10 @@ ], "summary": { "duration": 0.094338312, - "example_count": 5, + "example_count": 7, "failure_count": 2, "pending_count": 1, "errors_outside_of_examples_count": 0 }, - "summary_line": "5 examples, 2 failures, 1 pending" + "summary_line": "7 examples, 2 failures, 1 pending" } \ No newline at end of file diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts index 9b15b2b..a9ed562 100644 --- a/test/stubs/stubTestController.ts +++ b/test/stubs/stubTestController.ts @@ -9,7 +9,7 @@ export class StubTestController implements vscode.TestController { id: string = "stub_test_controller_id"; label: string = "stub_test_controller_label"; items: vscode.TestItemCollection - testRuns: Map = new Map() + testRuns: Map = new Map() readonly rootLog: IChildLogger constructor(readonly log: IChildLogger) { @@ -37,7 +37,7 @@ export class StubTestController implements vscode.TestController { persist?: boolean ): vscode.TestRun { let mockTestRun = mock() - this.testRuns.set(request, mockTestRun) + this.testRuns.set(request.profile!.label, mockTestRun) return instance(mockTestRun) } @@ -46,7 +46,7 @@ export class StubTestController implements vscode.TestController { } getMockTestRun(request: vscode.TestRunRequest): vscode.TestRun | undefined { - return this.testRuns.get(request) + return this.testRuns.get(request.profile!.label) } dispose = () => {} diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index cbc97bd..eb1b4ba 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -168,6 +168,7 @@ export function setupMockRequest(manager: TestSuiteManager, testId?: string | st when(mockRequest.exclude).thenReturn([]) let mockRunProfile = mock() when(mockRunProfile.label).thenReturn('Run') + when(mockRunProfile.kind).thenReturn(vscode.TestRunProfileKind.Run) when(mockRequest.profile).thenReturn(instance(mockRunProfile)) return mockRequest } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 719c3f6..9b12900 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -22,7 +22,7 @@ suite('Extension Test for Minitest', function() { let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; - const log = logger("debug"); + const log = logger("info"); let expectedPath = (file: string): string => { return path.resolve( @@ -78,7 +78,7 @@ suite('Extension Test for Minitest', function() { beforeEach(function () { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, true, workspaceFolder) + testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); }) @@ -101,7 +101,8 @@ suite('Extension Test for Minitest', function() { { file: expectedPath("abs_test.rb"), id: "abs_test.rb", - label: "Abs", + //label: "Abs", + label: "abs_test.rb", canResolveChildren: true, children: [] }, @@ -114,7 +115,8 @@ suite('Extension Test for Minitest', function() { { file: expectedPath("square/square_test.rb"), id: "square/square_test.rb", - label: "Square", + //label: "Square", + label: "square_test.rb", canResolveChildren: true, children: [] }, @@ -206,7 +208,7 @@ suite('Extension Test for Minitest', function() { before(async function() { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, true, workspaceFolder) + testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 58ab43f..0b458ed 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -66,13 +66,14 @@ suite('Extension Test for RSpec', function() { let contexts_many_expectation = { file: expectedPath('contexts_spec.rb'), id: 'contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]', - label: "doesn't break the extension", + //label: "doesn't break the extension", + label: "when there are many levels of nested contexts doesn't break the extension", line: 13, } let contexts_fewer_expectation = { file: expectedPath('contexts_spec.rb'), id: 'contexts_spec.rb[1:1:1:1:2:1]', - label: "still doesn't break the extension", + label: "when there are fewer levels of nested contexts test #1", line: 23, } @@ -90,21 +91,24 @@ suite('Extension Test for RSpec', function() { beforeEach(function () { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, false, workspaceFolder) + testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); }) test('Load tests on file resolve request', async function () { // Populate controller with test files. This would be done by the filesystem globs in the watchers - let createTest = (id: string, label?: string) => - testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) - let absSpecItem = createTest("abs_spec.rb") + let createTest = (id: string, canResolveChildren: boolean, label?: string) => { + let item = testController.createTestItem(id, label || id, vscode.Uri.file(expectedPath(id))) + item.canResolveChildren = canResolveChildren + return item + } + let absSpecItem = createTest("abs_spec.rb", true) testController.items.add(absSpecItem) - let contextsSpecItem = createTest("contexts_spec.rb") + let contextsSpecItem = createTest("contexts_spec.rb", true) testController.items.add(contextsSpecItem) - let subfolderItem = createTest("square") + let subfolderItem = createTest("square", true) testController.items.add(subfolderItem) - subfolderItem.children.add(createTest("square/square_spec.rb", "square_spec.rb")) + subfolderItem.children.add(createTest("square/square_spec.rb", true, "square_spec.rb")) // No tests in suite initially, just test files and folders testItemCollectionMatches(testController.items, @@ -113,23 +117,27 @@ suite('Extension Test for RSpec', function() { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", label: "abs_spec.rb", + canResolveChildren: true, children: [] }, { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb", label: "contexts_spec.rb", + canResolveChildren: true, children: [] }, { file: expectedPath("square"), id: "square", label: "square", + canResolveChildren: true, children: [ { file: expectedPath("square/square_spec.rb"), id: "square/square_spec.rb", label: "square_spec.rb", + canResolveChildren: true, children: [] }, ] @@ -171,6 +179,7 @@ suite('Extension Test for RSpec', function() { file: expectedPath("square/square_spec.rb"), id: "square/square_spec.rb", label: "square_spec.rb", + canResolveChildren: true, children: [] }, ] @@ -190,7 +199,8 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("abs_spec.rb"), id: "abs_spec.rb", - label: "Abs", + //label: "Abs", + label: "abs_spec.rb", canResolveChildren: true, children: [ abs_positive_expectation, @@ -201,52 +211,69 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb", - label: "Contexts", + //label: "Contexts", + label: "contexts_spec.rb", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1]", - label: "when", + //label: "when", + label: "contexts_spec.rb[1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1]", - label: "there", + //label: "there", + label: "contexts_spec.rb[1:1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1]", - label: "many", + //label: "are", + label: "contexts_spec.rb[1:1:1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1:1]", - label: "levels", + //label: "many", + label: "contexts_spec.rb[1:1:1:1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1:1:1]", - label: "of", + //label: "levels", + label: "contexts_spec.rb[1:1:1:1:1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1:1:1:1]", - label: "nested", + //label: "of", + label: "contexts_spec.rb[1:1:1:1:1:1:1]", canResolveChildren: true, children: [ { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1:1:1:1:1]", - label: "contexts", + //label: "nested", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1]", canResolveChildren: true, children: [ - contexts_many_expectation, + { + file: expectedPath("contexts_spec.rb"), + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + //label: "contexts", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + canResolveChildren: true, + children: [ + contexts_many_expectation, + ] + } ] }, ] @@ -258,7 +285,8 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("contexts_spec.rb"), id: "contexts_spec.rb[1:1:1:1:2]", - label: "fewer levels of nested contexts", + //label: "fewer levels of nested contexts", + label: "contexts_spec.rb[1:1:1:1:2]", canResolveChildren: true, children: [ contexts_fewer_expectation, @@ -281,7 +309,8 @@ suite('Extension Test for RSpec', function() { { file: expectedPath("square/square_spec.rb"), id: "square/square_spec.rb", - label: "Square", + //label: "Square", + label: "square_spec.rb", canResolveChildren: true, children: [ square_2_expectation, @@ -301,7 +330,7 @@ suite('Extension Test for RSpec', function() { before(async function() { testController = new StubTestController(log) manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, false, workspaceFolder) + testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader.discoverAllFilesInWorkspace() }) @@ -315,9 +344,9 @@ suite('Extension Test for RSpec', function() { await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.started(anything())).times(8) - verify(mockTestRun.passed(anything(), anything())).times(4) + verify(mockTestRun.enqueued(anything())).times(20) + verify(mockTestRun.started(anything())).times(7) + verify(mockTestRun.passed(anything(), anything())).times(8) verify(mockTestRun.failed(anything(), anything(), anything())).times(2) verify(mockTestRun.errored(anything(), anything(), anything())).times(2) verify(mockTestRun.skipped(anything())).times(2) @@ -329,8 +358,8 @@ suite('Extension Test for RSpec', function() { await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! - verify(mockTestRun.enqueued(anything())).times(0) - verify(mockTestRun.started(anything())).times(8) + verify(mockTestRun.enqueued(anything())).times(8) + verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) verify(mockTestRun.failed(anything(), anything(), anything())).times(2) verify(mockTestRun.errored(anything(), anything(), anything())).times(2) @@ -343,9 +372,9 @@ suite('Extension Test for RSpec', function() { await testRunner.runHandler(request, cancellationTokenSource.token) mockTestRun = (testController as StubTestController).getMockTestRun(request)! - verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.enqueued(anything())).times(7) // One less 'started' than the other tests as it doesn't include the 'square' folder - verify(mockTestRun.started(anything())).times(7) + verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) verify(mockTestRun.failed(anything(), anything(), anything())).times(2) verify(mockTestRun.errored(anything(), anything(), anything())).times(2) @@ -425,9 +454,7 @@ suite('Extension Test for RSpec', function() { } expect(testStateCaptors(mockTestRun).startedArg(0).id).to.eq(expectedTest.id) verify(mockTestRun.started(anything())).times(1) - - // Verify that no other status events occurred - verify(mockTestRun.enqueued(anything())).times(0) + verify(mockTestRun.enqueued(anything())).times(1) }) } }) diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 2aa6860..fa4fc1a 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -1,4 +1,4 @@ -import { before, beforeEach } from 'mocha'; +import { before, beforeEach, afterEach } from 'mocha'; import { instance, mock, when } from 'ts-mockito' import * as childProcess from 'child_process'; import * as vscode from 'vscode' @@ -11,7 +11,6 @@ import { FrameworkProcess } from '../../../src/frameworkProcess'; import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; import { logger } from '../../stubs/logger'; -import { StubTestController } from '../../stubs/stubTestController'; // JSON Fixtures import rspecDryRunOutput from '../../fixtures/unitTests/rspec/dryRunOutput.json' @@ -19,7 +18,7 @@ import rspecTestRunOutput from '../../fixtures/unitTests/rspec/testRunOutput.jso import minitestDryRunOutput from '../../fixtures/unitTests/minitest/dryRunOutput.json' import minitestTestRunOutput from '../../fixtures/unitTests/minitest/testRunOutput.json' -const log = logger("trace") +const log = logger("off") const cancellationTokenSoure = new vscode.CancellationTokenSource() suite('FrameworkProcess', function () { @@ -36,6 +35,12 @@ suite('FrameworkProcess', function () { when(mockContext.cancellationToken).thenReturn(cancellationTokenSoure.token) }) + afterEach(function() { + if (testController) { + testController.dispose() + } + }) + suite('#parseAndHandleTestOutput()', function () { suite('RSpec output', function () { before(function () { @@ -45,12 +50,144 @@ suite('FrameworkProcess', function () { }) beforeEach(function () { - testController = new StubTestController(log) + testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) }) const expectedTests: TestItemExpectation[] = [ + { + id: "abs_spec.rb", + //label: "Abs", + label: "abs_spec.rb", + file: path.resolve("spec", "abs_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "abs_spec.rb[1:1]", + label: "finds the absolute value of 1", + file: path.resolve("spec", "abs_spec.rb"), + line: 3, + }, + { + id: "abs_spec.rb[1:2]", + label: "finds the absolute value of 0", + file: path.resolve("spec", "abs_spec.rb"), + line: 7, + }, + { + id: "abs_spec.rb[1:3]", + label: "finds the absolute value of -1", + file: path.resolve("spec", "abs_spec.rb"), + line: 11, + } + ] + }, + { + id: "contexts_spec.rb", + //label: "Contexts", + label: "contexts_spec.rb", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1]", + //label: "when", + label: "contexts_spec.rb[1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1]", + //label: "there", + label: "contexts_spec.rb[1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1]", + //label: "are", + label: "contexts_spec.rb[1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1]", + //label: "many", + label: "contexts_spec.rb[1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1]", + //label: "levels", + label: "contexts_spec.rb[1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1]", + //label: "of", + label: "contexts_spec.rb[1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + //label: "nested", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + //label: "contexts", + label: "contexts_spec.rb[1:1:1:1:1:1:1:1:1]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:1:1:1:1:1:1]", + //label: "doesn't break the extension", + label: "when there are many levels of nested contexts doesn't break the extension", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: false, + line: 13, + }, + ] + }, + ] + }, + ] + }, + ] + }, + ] + }, + { + id: "contexts_spec.rb[1:1:1:1:2]", + //label: "fewer levels of nested contexts", + label: "contexts_spec.rb[1:1:1:1:2]", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: true, + children: [ + { + id: "contexts_spec.rb[1:1:1:1:2:1]", + label: "when there are fewer levels of nested contexts test #1", + file: path.resolve("spec", "contexts_spec.rb"), + canResolveChildren: false, + line: 23 + }, + ] + }, + ] + }, + ] + }, + ] + } + ] + }, { id: "square", label: "square", @@ -59,7 +196,8 @@ suite('FrameworkProcess', function () { children: [ { id: "square/square_spec.rb", - label: "Square", + //label: "Square", + label: "square_spec.rb", file: path.resolve("spec", "square", "square_spec.rb"), canResolveChildren: true, children: [ @@ -79,32 +217,6 @@ suite('FrameworkProcess', function () { } ] }, - { - id: "abs_spec.rb", - label: "Abs", - file: path.resolve("spec", "abs_spec.rb"), - canResolveChildren: true, - children: [ - { - id: "abs_spec.rb[1:1]", - label: "finds the absolute value of 1", - file: path.resolve("spec", "abs_spec.rb"), - line: 3, - }, - { - id: "abs_spec.rb[1:2]", - label: "finds the absolute value of 0", - file: path.resolve("spec", "abs_spec.rb"), - line: 7, - }, - { - id: "abs_spec.rb[1:3]", - label: "finds the absolute value of -1", - file: path.resolve("spec", "abs_spec.rb"), - line: 11, - } - ] - } ] test('parses dry run output correctly', function () { @@ -128,11 +240,13 @@ suite('FrameworkProcess', function () { }) beforeEach(function () { - testController = new StubTestController(log) + testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) }) + + const expectedTests: TestItemExpectation[] = [ { id: "square", @@ -142,7 +256,8 @@ suite('FrameworkProcess', function () { children: [ { id: "square/square_test.rb", - label: "Square", + //label: "Square", + label: "square_test.rb", file: path.resolve("test", "square", "square_test.rb"), canResolveChildren: true, children: [ @@ -164,7 +279,8 @@ suite('FrameworkProcess', function () { }, { id: "abs_test.rb", - label: "Abs", + //label: "Abs", + label: "abs_test.rb", file: path.resolve("test", "abs_test.rb"), canResolveChildren: true, children: [ diff --git a/test/suite/unitTests/testRunner.test.ts b/test/suite/unitTests/testRunner.test.ts deleted file mode 100644 index f20569d..0000000 --- a/test/suite/unitTests/testRunner.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -// import { before, beforeEach } from 'mocha'; -// import { instance, mock, when } from 'ts-mockito' -// import * as vscode from 'vscode' -// import * as path from 'path' - -// import { Config } from "../../../src/config"; -// import { TestSuiteManager } from "../../testSuiteManager"; -// import { TestRunner } from "../../../src/testRunner"; -// import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; -// import { logger } from '../../stubs/logger'; -// import { StubTestController } from '../../stubs/stubTestController'; - -// const log = logger("off") - -// suite('TestRunner', function () { -// let manager: TestSuiteManager -// let testController: vscode.TestController -// let testRunner: TestRunner - -// const config = mock() - -// suite('#parseAndHandleTestOutput()', function () { -// suite('RSpec output', function () { -// before(function () { -// let relativeTestPath = "spec" -// when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) -// when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) -// }) - -// beforeEach(function () { -// testController = new StubTestController(log) -// manager = new TestSuiteManager(log, testController, instance(config)) -// testRunner = new TestRunner(log, manager, false) -// }) - -// const expectedTests: TestItemExpectation[] = [ -// { -// id: "square", -// label: "square", -// file: path.resolve("spec", "square"), -// canResolveChildren: true, -// children: [ -// { -// id: "square/square_spec.rb", -// label: "square_spec.rb", -// file: path.resolve("spec", "square", "square_spec.rb"), -// canResolveChildren: true, -// children: [ -// { -// id: "square/square_spec.rb[1:1]", -// label: "finds the square of 2", -// file: path.resolve("spec", "square", "square_spec.rb"), -// line: 3, -// }, -// { -// id: "square/square_spec.rb[1:2]", -// label: "finds the square of 3", -// file: path.resolve("spec", "square", "square_spec.rb"), -// line: 7, -// }, -// ] -// } -// ] -// }, -// { -// id: "abs_spec.rb", -// label: "abs_spec.rb", -// file: path.resolve("spec", "abs_spec.rb"), -// canResolveChildren: true, -// children: [ -// { -// id: "abs_spec.rb[1:1]", -// label: "finds the absolute value of 1", -// file: path.resolve("spec", "abs_spec.rb"), -// line: 3, -// }, -// { -// id: "abs_spec.rb[1:2]", -// label: "finds the absolute value of 0", -// file: path.resolve("spec", "abs_spec.rb"), -// line: 7, -// }, -// { -// id: "abs_spec.rb[1:3]", -// label: "finds the absolute value of -1", -// file: path.resolve("spec", "abs_spec.rb"), -// line: 11, -// } -// ] -// } -// ] -// const outputJson = { -// "version":"3.10.1", -// "examples":[ -// {"id":"./spec/square/square_spec.rb[1:1]","description":"finds the square of 2","full_description":"Square finds the square of 2","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":4,"type":null,"pending_message":null}, -// {"id":"./spec/square/square_spec.rb[1:2]","description":"finds the square of 3","full_description":"Square finds the square of 3","status":"passed","file_path":"./spec/square/square_spec.rb","line_number":8,"type":null,"pending_message":null}, -// {"id":"./spec/abs_spec.rb[1:1]","description":"finds the absolute value of 1","full_description":"Abs finds the absolute value of 1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":4,"type":null,"pending_message":null}, -// {"id":"./spec/abs_spec.rb[1:2]","description":"finds the absolute value of 0","full_description":"Abs finds the absolute value of 0","status":"passed","file_path":"./spec/abs_spec.rb","line_number":8,"type":null,"pending_message":null}, -// {"id":"./spec/abs_spec.rb[1:3]","description":"finds the absolute value of -1","full_description":"Abs finds the absolute value of -1","status":"passed","file_path":"./spec/abs_spec.rb","line_number":12,"type":null,"pending_message":null} -// ], -// "summary":{"duration":0.006038228,"example_count":6,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0}, -// "summary_line":"6 examples, 0 failures" -// } -// const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - -// test('parses specs correctly', function () { -// testRunner.parseAndHandleTestOutput(output) -// testItemCollectionMatches(testController.items, expectedTests) -// }) -// }) - -// suite('Minitest output', function () { -// before(function () { -// let relativeTestPath = "test" -// when(config.getRelativeTestDirectory()).thenReturn(relativeTestPath) -// when(config.getAbsoluteTestDirectory()).thenReturn(path.resolve(relativeTestPath)) -// }) - -// beforeEach(function () { -// testController = new StubTestController(log) -// manager = new TestSuiteManager(log, testController, instance(config)) -// testRunner = new TestRunner(log, manager, true) -// }) - -// const expectedTests: TestItemExpectation[] = [ -// { -// id: "square", -// label: "square", -// file: path.resolve("test", "square"), -// canResolveChildren: true, -// children: [ -// { -// id: "square/square_test.rb", -// label: "square_test.rb", -// file: path.resolve("test", "square", "square_test.rb"), -// canResolveChildren: true, -// children: [ -// { -// id: "square/square_test.rb[4]", -// label: "square 2", -// file: path.resolve("test", "square", "square_test.rb"), -// line: 3, -// }, -// { -// id: "square/square_test.rb[8]", -// label: "square 3", -// file: path.resolve("test", "square", "square_test.rb"), -// line: 7, -// }, -// ] -// } -// ] -// }, -// { -// id: "abs_test.rb", -// label: "abs_test.rb", -// file: path.resolve("test", "abs_test.rb"), -// canResolveChildren: true, -// children: [ -// { -// id: "abs_test.rb[4]", -// label: "abs positive", -// file: path.resolve("test", "abs_test.rb"), -// line: 3, -// }, -// { -// id: "abs_test.rb[8]", -// label: "abs 0", -// file: path.resolve("test", "abs_test.rb"), -// line: 7, -// }, -// { -// id: "abs_test.rb[12]", -// label: "abs negative", -// file: path.resolve("test", "abs_test.rb"), -// line: 11, -// } -// ] -// }, -// ] -// const outputJson = { -// "version":"5.14.4", -// "examples":[ -// {"description":"abs positive","full_description":"abs positive","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":4,"klass":"AbsTest","method":"test_abs_positive","runnable":"AbsTest","id":"./test/abs_test.rb[4]"}, -// {"description":"abs 0","full_description":"abs 0","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":8,"klass":"AbsTest","method":"test_abs_0","runnable":"AbsTest","id":"./test/abs_test.rb[8]"}, -// {"description":"abs negative","full_description":"abs negative","file_path":"./test/abs_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/abs_test.rb","line_number":12,"klass":"AbsTest","method":"test_abs_negative","runnable":"AbsTest","id":"./test/abs_test.rb[12]"}, -// {"description":"square 2","full_description":"square 2","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":4,"klass":"SquareTest","method":"test_square_2","runnable":"SquareTest","id":"./test/square/square_test.rb[4]"}, -// {"description":"square 3","full_description":"square 3","file_path":"./test/square/square_test.rb","full_path":"/home/tabby/git/vscode-ruby-test-adapter/test/fixtures/minitest/test/square/square_test.rb","line_number":8,"klass":"SquareTest","method":"test_square_3","runnable":"SquareTest","id":"./test/square/square_test.rb[8]"} -// ] -// } -// const output = `START_OF_TEST_JSON${JSON.stringify(outputJson)}END_OF_TEST_JSON` - -// test('parses specs correctly', function () { -// testRunner.parseAndHandleTestOutput(output) -// testItemCollectionMatches(testController.items, expectedTests) -// }) -// }) -// }) -// }) diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts index cfed424..f240f06 100644 --- a/test/suite/unitTests/testSuiteManager.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -1,18 +1,17 @@ import { expect } from 'chai'; -import { before, beforeEach } from 'mocha'; +import { before, beforeEach, afterEach } from 'mocha'; import { instance, mock, when } from 'ts-mockito' import * as vscode from 'vscode' import path from 'path' import { Config } from '../../../src/config'; import { TestSuiteManager } from '../../../src/testSuiteManager'; -import { StubTestController } from '../../stubs/stubTestController'; import { logger } from '../../stubs/logger'; import { testUriMatches } from '../helpers'; -const log = logger("trace") +const log = logger("off") -suite('TestSuite', function () { +suite('TestSuiteManager', function () { let mockConfig: Config = mock(); const config: Config = instance(mockConfig) let controller: vscode.TestController; @@ -25,10 +24,14 @@ suite('TestSuite', function () { }); beforeEach(function () { - controller = new StubTestController(log) + controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, controller, instance(mockConfig)) }); + afterEach(function() { + controller.dispose() + }) + suite('#normaliseTestId()', function () { const parameters = [ { arg: 'test-id', expected: 'test-id' }, @@ -120,9 +123,9 @@ suite('TestSuite', function () { }) suite('#getOrCreateTestItem()', function () { - const id = 'test-id' + const id = 'test-id.rb' const label = 'test-label' - const childId = `folder${path.sep}child-test` + const childId = path.join('folder', 'child-test.rb') let testItem: vscode.TestItem let childItem: vscode.TestItem @@ -223,6 +226,54 @@ suite('TestSuite', function () { }) } }) + + suite('sets canResolveChildren correctly when creating items', function() { + test('folder', function() { + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('ruby file', function() { + expect(manager.getOrCreateTestItem('file.rb').canResolveChildren).to.eq(true) + }); + + test('folder and file', function() { + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('test case in file with context (rspec)', function() { + expect(manager.getOrCreateTestItem('file.rb[1:1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('file.rb[1:1]').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('file.rb').canResolveChildren).to.eq(true) + }); + + test('test case in file (minitest)', function() { + expect(manager.getOrCreateTestItem('file.rb[1]').canResolveChildren).to.eq(false) + }); + + test('test case in file (rspec)', function() { + expect(manager.getOrCreateTestItem('file.rb[1:1]').canResolveChildren).to.eq(false) + }); + + test('folder and test case in file with context (rspec)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1:1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb[1:1]').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('folder and test case in file (minitest)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + + test('folder and test case in file (rspec)', function() { + expect(manager.getOrCreateTestItem('folder/file.rb[1:1]').canResolveChildren).to.eq(false) + expect(manager.getOrCreateTestItem('folder/file.rb').canResolveChildren).to.eq(true) + expect(manager.getOrCreateTestItem('folder').canResolveChildren).to.eq(true) + }); + }) }) suite('#getParentIdsFromId', function() { From 26a80c9b1324c7167d18ba831396cac804adda4c Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 10:02:41 +0000 Subject: [PATCH 070/108] Make test resolution actually lazy --- src/main.ts | 9 ++++++--- src/testLoader.ts | 3 +-- test/suite/minitest/minitest.test.ts | 4 ++-- test/suite/rspec/rspec.test.ts | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main.ts b/src/main.ts index 0dafe17..379733a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,17 +101,20 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(profiles.resolveTestsProfile); context.subscriptions.push(factory); - factory.getLoader().discoverAllFilesInWorkspace(); - controller.resolveHandler = async test => { log.debug('resolveHandler called', test) if (!test) { await factory.getLoader().discoverAllFilesInWorkspace(); - } else if (test.id.endsWith(".rb")) { + } else if (test.id.endsWith(".rb") || test.id.endsWith(']')) { // Only parse files + if (!test.canResolveChildren) { + log.warn("resolveHandler called for test that can't resolve children", test.id) + } await factory.getLoader().loadTestItem(test); } }; + + factory.getLoader().discoverAllFilesInWorkspace(); } else { log.fatal('No test framework detected. Configure the rubyTestExplorer.testFramework setting if you want to use the Ruby Test Explorer.'); diff --git a/src/testLoader.ts b/src/testLoader.ts index 3035f31..5f42ed8 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -99,8 +99,7 @@ export class TestLoader implements vscode.Disposable { let fileWatchers = await Promise.all(patterns.map(async (pattern) => { for (const file of await vscode.workspace.findFiles(pattern)) { log.debug('Found file, creating TestItem', file) - // Enqueue the file to load tests from it - resolveFilesPromises.push(this.resolveQueue.enqueue(this.manager.getOrCreateTestItem(this.uriToTestId(file)))) + this.manager.getOrCreateTestItem(this.uriToTestId(file)) } // TODO - skip if filewatcher for this pattern exists and dispose filewatchers for patterns no longer in config diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 9b12900..4fa8956 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -162,7 +162,7 @@ suite('Extension Test for Minitest', function() { }) test('Load all tests', async function () { - await testLoader.discoverAllFilesInWorkspace() + await testLoader['loadTests']() const manager = testController.items @@ -210,7 +210,7 @@ suite('Extension Test for Minitest', function() { manager = new TestSuiteManager(log, testController, config) testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); - await testLoader.discoverAllFilesInWorkspace() + await testLoader['loadTests']() }) suite(`running collections emits correct statuses`, async function() { diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 0b458ed..5d79440 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -190,7 +190,7 @@ suite('Extension Test for RSpec', function() { test('Load all tests', async function () { console.log("resolving files in test") - await testLoader.discoverAllFilesInWorkspace() + await testLoader['loadTests']() const manager = testController.items @@ -332,7 +332,7 @@ suite('Extension Test for RSpec', function() { manager = new TestSuiteManager(log, testController, config) testRunner = new TestRunner(log, manager, workspaceFolder) testLoader = new TestLoader(log, resolveTestsProfile, manager); - await testLoader.discoverAllFilesInWorkspace() + await testLoader['loadTests']() }) suite(`running collections emits correct statuses`, async function() { From c09a7ad75b03baa48c1b23c49c330689972d94cb Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 11:24:27 +0000 Subject: [PATCH 071/108] Change queue from an array to a set --- src/loaderQueue.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts index ca54550..9073284 100644 --- a/src/loaderQueue.ts +++ b/src/loaderQueue.ts @@ -28,7 +28,7 @@ export type QueueItem = { */ export class LoaderQueue implements vscode.Disposable { private readonly log: IChildLogger - private readonly queue: QueueItem[] = [] + private readonly queue: Set = new Set() private isDisposed = false private notifyQueueWorker?: () => void private terminateQueueWorker?: () => void @@ -79,7 +79,7 @@ export class LoaderQueue implements vscode.Disposable { queueItem["resolve"] = () => resolve(item) queueItem["reject"] = reject }) - this.queue.push(queueItem) + this.queue.add(queueItem) if (this.notifyQueueWorker) { this.log.debug('notifying worker of items in queue') // Notify the worker function that there are items to resolve if it's waiting @@ -93,7 +93,7 @@ export class LoaderQueue implements vscode.Disposable { log.info('worker started') // Check to see if the queue is being disposed while(!this.isDisposed) { - if (this.queue.length == 0) { + if (this.queue.size == 0) { log.debug('awaiting items to resolve') // While the queue is empty, wait for more items await new Promise((resolve, reject) => { @@ -112,8 +112,8 @@ export class LoaderQueue implements vscode.Disposable { this.terminateQueueWorker = undefined } else { // Drain queue to get batch of test items to load - let queueItems: QueueItem[] = [] - while(this.queue.length > 0) { queueItems.push(this.queue.pop()!) } + 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)) From f68262b34c1f7c2e306749c688aaf178f25770c7 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 10:08:17 +0000 Subject: [PATCH 072/108] Fix log issues * Remove test IDs and URLs from logger labels * Use format strings in log messages * Add log level configuration option * Update config change handler in TestLoader to only care about changes that affect it * Change amount of information logged in a few places --- package.json | 15 +++++++++++++++ src/frameworkProcess.ts | 5 ++++- src/loaderQueue.ts | 4 ++-- src/main.ts | 8 ++++---- src/testLoader.ts | 21 ++++++++++++++------- src/testRunContext.ts | 18 +++++++++--------- src/testRunner.ts | 22 +++++++++++----------- src/testSuiteManager.ts | 32 ++++++++++++-------------------- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 9ceaace..4a2f161 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,21 @@ "type": "string", "scope": "resource" }, + "rubyTestExplorer.logLevel": { + "description": "Level of information to include in logs", + "type": "string", + "scope": "resource", + "default": "info", + "enum": [ + "off", + "fatal", + "error", + "warn", + "info", + "debug", + "trace" + ] + }, "rubyTestExplorer.testFramework": { "description": "Test framework to use by default, for example rspec or minitest.", "type": "string", diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 9564b9c..3e16b07 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -198,6 +198,9 @@ export class FrameworkProcess implements vscode.Disposable { testFile.children.replace(parsedTests.filter(x => !x.canResolveChildren && x.parent == testFile)) } } + if (testMetadata.summary) { + log.info('Test run completed in %d ms', testMetadata.summary.duration) + } } private parseDescription(test: ParsedTest): string { @@ -258,7 +261,7 @@ export class FrameworkProcess implements vscode.Disposable { this.testStatusEmitter.fire(new TestStatus(testItem, status, parsedtest.duration, this.failureMessage(testItem, parsedtest))) break; default: - log.error('Unexpected test status', status, testItem.id) + log.error('Unexpected test status %s for test ID %s', status, testItem.id) } } diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts index 9073284..458607c 100644 --- a/src/loaderQueue.ts +++ b/src/loaderQueue.ts @@ -35,7 +35,7 @@ export class LoaderQueue implements vscode.Disposable { public readonly worker: Promise constructor(rootLog: IChildLogger, private readonly processItems: (testItems?: vscode.TestItem[]) => Promise) { - this.log = rootLog.getChildLogger({label: 'ResolveQueue'}) + this.log = rootLog.getChildLogger({label: `${LoaderQueue.name}`}) this.worker = this.resolveItemsInQueueWorker() } @@ -67,7 +67,7 @@ export class LoaderQueue implements vscode.Disposable { * 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', item.id) + this.log.debug('enqueing item to resolve: %s', item.id) // Create queue item with empty functions let queueItem: QueueItem = { item: item, diff --git a/src/main.ts b/src/main.ts index 379733a..9b7573d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,12 @@ import * as vscode from 'vscode'; -import { getExtensionLogger, IChildLogger } from "@vscode-logging/logger"; +import { getExtensionLogger, IChildLogger, LogLevel } from "@vscode-logging/logger"; import { TestFactory } from './testFactory'; import { RspecConfig } from './rspec/rspecConfig'; import { MinitestConfig } from './minitest/minitestConfig'; import { Config } from './config'; export const guessWorkspaceFolder = async (rootLog: IChildLogger) => { - let log = rootLog.getChildLogger({ label: "guessWorkspaceFolder: " }) + let log = rootLog.getChildLogger({ label: "guessWorkspaceFolder" }) if (!vscode.workspace.workspaceFolders) { return undefined; } @@ -36,7 +36,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(logOutputChannel) const log = getExtensionLogger({ extName: "RubyTestExplorer", - level: "debug", // See LogLevel type in @vscode-logging/types for possible logLevels + level: (extensionConfig.get('logLevel') as LogLevel), // See LogLevel type in @vscode-logging/types for possible logLevels logPath: context.logUri.fsPath, // The logPath is only available from the `vscode.ExtensionContext` logOutputChannel: logOutputChannel, // OutputChannel for the logger sourceLocationTracking: false, @@ -108,7 +108,7 @@ export async function activate(context: vscode.ExtensionContext) { } else if (test.id.endsWith(".rb") || test.id.endsWith(']')) { // Only parse files if (!test.canResolveChildren) { - log.warn("resolveHandler called for test that can't resolve children", test.id) + log.warn("resolveHandler called for test that can't resolve children: %s", test.id) } await factory.getLoader().loadTestItem(test); } diff --git a/src/testLoader.ts b/src/testLoader.ts index 5f42ed8..3341480 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -53,13 +53,13 @@ export class TestLoader implements vscode.Disposable { // When files are created, make sure there's a corresponding "file" node in the test item tree watcher.onDidCreate(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidCreate watcher'}) - watcherLog.debug('File created', uri.fsPath) + watcherLog.debug('File created: %s', uri.fsPath) this.manager.getOrCreateTestItem(this.uriToTestId(uri)) }) // When files change, reload them watcher.onDidChange(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidChange watcher'}) - watcherLog.debug('File changed, reloading tests', uri.fsPath) + watcherLog.debug('File changed, reloading tests: %s', uri.fsPath) let testItem = this.manager.getTestItem(this.uriToTestId(uri)) if (!testItem) { watcherLog.error('Unable to find test item for file', uri) @@ -70,7 +70,7 @@ export class TestLoader implements vscode.Disposable { // And, finally, delete TestItems for removed files watcher.onDidDelete(uri => { let watcherLog = this.log.getChildLogger({label: 'onDidDelete watcher'}) - watcherLog.debug('File deleted', uri.fsPath) + watcherLog.debug('File deleted: %s', uri.fsPath) this.manager.deleteTestItem(this.uriToTestId(uri)) }); @@ -143,28 +143,35 @@ export class TestLoader implements vscode.Disposable { log.debug('configWatcher') return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); - if (configChange.affectsConfiguration('rubyTestExplorer')) { + if (this.configChangeAffectsFileWatchers(configChange)) { this.manager.controller.items.replace([]) this.discoverAllFilesInWorkspace(); } }) } + private configChangeAffectsFileWatchers(configChange: vscode.ConfigurationChangeEvent): boolean { + return configChange.affectsConfiguration('rubyTestExplorer.filePattern') + || configChange.affectsConfiguration('rubyTestExplorer.testFramework') + || configChange.affectsConfiguration('rubyTestExplorer.rspecDirectory') + || configChange.affectsConfiguration('rubyTestExplorer.minitestDirectory') + } + /** * Converts a test URI into a test ID * @param uri URI of test * @returns test ID */ private uriToTestId(uri: string | vscode.Uri): string { - let log = this.log.getChildLogger({label: `uriToTestId(${uri})`}) + let log = this.log.getChildLogger({label: 'uriToTestId'}) if (typeof uri === "string") { log.debug("uri is string. Returning unchanged") return uri } let fullTestDirPath = this.manager.config.getAbsoluteTestDirectory() - log.debug('Full path to test dir', fullTestDirPath) + log.debug('Full path to test dir: %s', fullTestDirPath) let strippedUri = uri.fsPath.replace(fullTestDirPath + path.sep, '') - log.debug('Stripped URI', strippedUri) + log.debug('Stripped URI: %s', strippedUri) return strippedUri } } diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 4887efb..4325c7d 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -26,8 +26,8 @@ export class TestRunContext { readonly controller: vscode.TestController, public readonly debuggerConfig?: vscode.DebugConfiguration ) { - this.log = rootLog.getChildLogger({ label: `TestRunContext(${request.profile?.label})` }) - this.log.info('Creating test run'); + this.log = rootLog.getChildLogger({ label: `${TestRunContext.name}(${request.profile?.label})` }) + this.log.info('Creating test run', {request: request}); this.testRun = controller.createTestRun(request) } @@ -37,7 +37,7 @@ export class TestRunContext { * @param test Test item to update. */ public enqueued(test: vscode.TestItem): void { - this.log.debug('Enqueued test', test.id) + this.log.debug('Enqueued test: %s', test.id) this.testRun.enqueued(test) } @@ -55,7 +55,7 @@ export class TestRunContext { message: vscode.TestMessage, duration?: number ): void { - this.log.debug('Errored test', test.id, duration, message.message) + this.log.debug('Errored test: %s (duration: %d)', test.id, duration, message) this.testRun.errored(test, message, duration) } @@ -73,7 +73,7 @@ export class TestRunContext { message: vscode.TestMessage, duration?: number ): void { - this.log.debug('Failed test', test.id, duration, message.message) + this.log.debug('Failed test: %s (duration: %d)', test.id, duration, message) this.testRun.failed(test, message, duration) } @@ -86,7 +86,7 @@ export class TestRunContext { public passed(test: vscode.TestItem, duration?: number | undefined ): void { - this.log.debug('Passed test', test.id, duration) + this.log.debug('Passed test: %s (duration: %d)', test.id, duration) this.testRun.passed(test, duration) } @@ -96,7 +96,7 @@ export class TestRunContext { * @param test ID of the test item to update, or the test item. */ public skipped(test: vscode.TestItem): void { - this.log.debug('Skipped test', test.id) + this.log.debug('Skipped test: %s', test.id) this.testRun.skipped(test) } @@ -106,12 +106,12 @@ export class TestRunContext { * @param test Test item to update, or the test item. */ public started(test: vscode.TestItem): void { - this.log.debug('Started test', test.id) + this.log.debug('Started test: %s', test.id) this.testRun.started(test) } public endTestRun(): void { - this.log.debug('Ending test run'); + this.log.info('Ending test run'); this.testRun.end() } } diff --git a/src/testRunner.ts b/src/testRunner.ts index 885ad3c..9b45914 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -133,7 +133,7 @@ export class TestRunner implements vscode.Disposable { log.debug("Running selected tests") command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) for (const node of testsToRun) { - log.trace("Adding test to command", node.id) + log.trace('Adding test to command: %s', node.id) // Mark selected tests as started this.enqueTestAndChildren(node, context) command = `${command} ${node.uri?.fsPath}` @@ -144,7 +144,7 @@ export class TestRunner implements vscode.Disposable { } command = `${command}:${node.range!.start.line + 1}` } - log.trace("Current command", command) + log.trace("Current command: %s", command) } } if (debuggerConfig) { @@ -161,7 +161,7 @@ export class TestRunner implements vscode.Disposable { } finally { // Make sure to end the run after all tests have been executed: - log.info('Ending test run'); + log.debug('Ending test run'); context.endTestRun(); } if (token.isCancellationRequested) { @@ -246,11 +246,11 @@ export class TestRunner implements vscode.Disposable { env: this.manager.config.getProcessEnv() }; - this.log.debug('Running command', testCommand); + this.log.info('Running command: %s', testCommand); let testProfileKind = context.request.profile!.kind if (this.testProcessMap.get(testProfileKind)) { - this.log.warn('Test run already in progress for profile kind', testProfileKind) + this.log.warn('Test run already in progress for profile kind: %s', testProfileKind) return } let testProcess = new FrameworkProcess(this.log, testCommand, spawnArgs, context, this.manager) @@ -261,27 +261,27 @@ export class TestRunner implements vscode.Disposable { let log = this.log.getChildLogger({label: 'testStatusListener'}) switch(event.status) { case Status.skipped: - log.debug('Received test skipped event', event.testItem.id) + log.debug('Received test skipped event: %s', event.testItem.id) context.skipped(event.testItem) break; case Status.passed: - log.debug('Received test passed event', event.testItem.id, event.duration) + log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) context.passed(event.testItem, event.duration) break; case Status.errored: - log.debug('Received test errored event', event.testItem.id, event.duration, event.message) + log.debug('Received test errored event: %s (duration: %d)', event.testItem.id, event.duration, event.message) context.errored(event.testItem, event.message!, event.duration) break; case Status.failed: - log.debug('Received test failed event', event.testItem.id, event.duration, event.message) + log.debug('Received test failed event: %s (duration: %d)', event.testItem.id, event.duration, event.message) context.failed(event.testItem, event.message!, event.duration) break; case Status.running: - log.debug('Received test started event', event.testItem.id) + log.debug('Received test started event: %s', event.testItem.id) context.started(event.testItem) break; default: - log.warn('Unexpected status', event.status) + log.warn('Unexpected status: %s', event.status) } }) try { diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index 67021c4..bf5c6cb 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -18,22 +18,22 @@ export class TestSuiteManager { public readonly controller: vscode.TestController, public readonly config: Config ) { - this.log = rootLog.getChildLogger({label: 'TestSuite'}); + this.log = rootLog.getChildLogger({label: `${TestSuiteManager.name}`}); } public deleteTestItem(testId: string) { let log = this.log.getChildLogger({label: 'deleteTestItem'}) testId = this.normaliseTestId(testId) - log.debug('Deleting test', testId) + log.debug('Deleting test: %s', testId) let testItem = this.getTestItem(testId) if (!testItem) { - log.error('No test item found with given ID', testId) + log.error('No test item found with given ID: %s', testId) return } let collection = testItem.parent ? testItem.parent.children : this.controller.items if (collection) { collection.delete(testId); - log.debug('Removed test', testId) + log.debug('Removed test: %s', testId) } else { log.error('Parent collection not found') } @@ -68,7 +68,7 @@ export class TestSuiteManager { * - Removes leading test dir if present */ public normaliseTestId(testId: string): string { - let log = this.log.getChildLogger({label: `normaliseTestId(${testId})`}) + let log = this.log.getChildLogger({label: `${this.normaliseTestId.name}`}) if (testId.startsWith(`.${path.sep}`)) { testId = testId.substring(2) } @@ -78,7 +78,7 @@ export class TestSuiteManager { if (testId.startsWith(path.sep)) { testId = testId.substring(1) } - log.debug('Normalised ID', testId) + log.debug('Normalised ID: %s', testId) return testId } @@ -102,13 +102,13 @@ export class TestSuiteManager { onItemCreated: TestItemCallback = (_) => {}, canResolveChildren: boolean = true, ): vscode.TestItem { - let log = this.log.getChildLogger({ label: `${this.createTestItem.name}(${testId})` }) + let log = this.log.getChildLogger({ label: `${this.createTestItem.name}` }) let uri = this.testIdToUri(testId) log.debug('Creating test item', {label: label, parentId: parent?.id, canResolveChildren: canResolveChildren, uri: uri}) let item = this.controller.createTestItem(testId, label, uri) item.canResolveChildren = canResolveChildren; (parent?.children || this.controller.items).add(item); - log.debug('Added test', item.id) + log.debug('Added test: %s', item.id) onItemCreated(item) return item } @@ -119,7 +119,7 @@ export class TestSuiteManager { * @returns array of test IDs */ private getParentIdsFromId(testId: string): string[] { - let log = this.log.getChildLogger({label: `${this.getParentIdsFromId.name}(${testId})`}) + let log = this.log.getChildLogger({label: `${this.getParentIdsFromId.name}`}) testId = this.normaliseTestId(testId) // Split path segments @@ -128,32 +128,24 @@ export class TestSuiteManager { if (idSegments[0] === "") { idSegments.splice(0, 1) } - log.trace('ID segments split by path', idSegments) for (let i = 1; i < idSegments.length - 1; i++) { - let currentSegment = idSegments[i] let precedingSegments = idSegments.slice(0, i + 1) - log.trace(`segment: ${currentSegment}. preceding segments`, precedingSegments) idSegments[i] = path.join(...precedingSegments) } - log.trace('ID segments joined with preceding segments', idSegments) // Split location const match = idSegments.at(-1)?.match(/(?[^\[]*)(?:\[(?[0-9:]+)\])?/) if (match && match.groups) { // Get file ID (with path to it if there is one) let fileId = match.groups["fileId"] - log.trace('Filename', fileId) if (idSegments.length > 1) { fileId = path.join(idSegments.at(-2)!, fileId) - log.trace('Filename with path', fileId) } // Add file ID to array idSegments.splice(-1, 1, fileId) - log.trace('ID segments with file ID inserted', idSegments) if (match.groups["location"]) { let locations = match.groups["location"].split(':') - log.trace('ID location segments', locations) if (locations.length == 1) { // Insert ID for minitest location let contextId = `${fileId}[${locations[0]}]` @@ -165,9 +157,9 @@ export class TestSuiteManager { idSegments.push(contextId) } } - log.trace('ID segments with location IDs appended', idSegments) } } + log.trace('Final ID segments list', idSegments) return idSegments } @@ -179,7 +171,7 @@ export class TestSuiteManager { ): vscode.TestItem | undefined { testId = this.normaliseTestId(testId) - log.debug('Looking for test', testId) + log.debug('Looking for test: %s', testId) let parentIds = this.getParentIdsFromId(testId) let item: vscode.TestItem | undefined = undefined let itemCollection: vscode.TestItemCollection = this.controller.items @@ -187,7 +179,7 @@ export class TestSuiteManager { // Walk through test folders to find the collection containing our test file, // creating parent items as needed for (const id of parentIds) { - log.debug('Getting item from parent collection', id, item?.id || 'controller') + log.debug('Getting item %s from parent collection %s', id, item?.id || 'controller') let child = itemCollection.get(id) if (!child) { if (createIfMissing) { From f50a876bdfff7c1ad1fdbc856ed5ee1c860bd6c7 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 11:53:06 +0000 Subject: [PATCH 073/108] Finally fix tests being loaded twice >_< --- src/testRunner.ts | 60 +++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index 9b45914..96e4e82 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -120,40 +120,44 @@ export class TestRunner implements vscode.Disposable { let command: string if (context.request.profile?.label === 'ResolveTests') { + // Load tests command = this.manager.config.getResolveTestsCommand(testsToRun) await this.runTestFramework(command, context) - } else if (!testsToRun) { - log.debug("Running all tests") - this.manager.controller.items.forEach((item) => { - // Mark selected tests as started - this.enqueTestAndChildren(item, context) - }) - command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) } else { - log.debug("Running selected tests") - command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) - for (const node of testsToRun) { - log.trace('Adding test to command: %s', node.id) - // Mark selected tests as started - this.enqueTestAndChildren(node, context) - command = `${command} ${node.uri?.fsPath}` - if (!node.canResolveChildren) { - // single test - if (!node.range) { - throw new Error(`Test item is missing line number: ${node.id}`) + // Run tests + if (!testsToRun) { + log.debug("Running all tests") + this.manager.controller.items.forEach((item) => { + // Mark selected tests as started + this.enqueTestAndChildren(item, context) + }) + command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) + } else { + log.debug("Running selected tests") + command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) + for (const node of testsToRun) { + log.trace('Adding test to command: %s', node.id) + // Mark selected tests as started + this.enqueTestAndChildren(node, context) + command = `${command} ${node.uri?.fsPath}` + if (!node.canResolveChildren) { + // single test + if (!node.range) { + throw new Error(`Test item is missing line number: ${node.id}`) + } + command = `${command}:${node.range!.start.line + 1}` } - command = `${command}:${node.range!.start.line + 1}` + log.trace("Current command: %s", command) } - log.trace("Current command: %s", command) } - } - if (debuggerConfig) { - log.debug('Debugging tests', request.include?.map(x => x.id)); - await Promise.all([this.startDebugSession(debuggerConfig, context), this.runTestFramework(command, context)]) - } - else { - log.debug('Running test', request.include?.map(x => x.id)); - await this.runTestFramework(command, context) + if (debuggerConfig) { + log.debug('Debugging tests', request.include?.map(x => x.id)); + await Promise.all([this.startDebugSession(debuggerConfig, context), this.runTestFramework(command, context)]) + } + else { + log.debug('Running test', request.include?.map(x => x.id)); + await this.runTestFramework(command, context) + } } } catch (err) { From d38c7740b8f9c9e76db5585620013fd0b168e158 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 12:43:15 +0000 Subject: [PATCH 074/108] Mark items as busy when loading them --- src/testLoader.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/testLoader.ts b/src/testLoader.ts index 3341480..1e045eb 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -121,11 +121,14 @@ export class TestLoader implements vscode.Disposable { let log = this.log.getChildLogger({label:'loadTests'}) log.info('Loading tests...', testItems?.map(x => x.id) || 'all tests'); try { + if (testItems) { for (const item of testItems) { item.busy = true }} let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) } catch (e: any) { log.error('Failed to load tests', e) return Promise.reject(e) + } finally { + if (testItems) { for (const item of testItems) { item.busy = false }} } } From d12e9a336110795b02bf648fc148c410027ef51f Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 13:46:08 +0000 Subject: [PATCH 075/108] Don't mark tests as passed during test loading --- src/testRunner.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index 96e4e82..04bcb41 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -269,8 +269,12 @@ export class TestRunner implements vscode.Disposable { context.skipped(event.testItem) break; case Status.passed: - log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) - context.passed(event.testItem, event.duration) + if (this.isTestLoad(context)) { + log.debug('Ignored test passed event from test load: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) + context.passed(event.testItem, event.duration) + } break; case Status.errored: log.debug('Received test errored event: %s (duration: %d)', event.testItem.id, event.duration, event.message) @@ -281,8 +285,12 @@ export class TestRunner implements vscode.Disposable { context.failed(event.testItem, event.message!, event.duration) break; case Status.running: - log.debug('Received test started event: %s', event.testItem.id) - context.started(event.testItem) + if (this.isTestLoad(context)) { + log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.debug('Received test started event: %s', event.testItem.id) + context.started(event.testItem) + } break; default: log.warn('Unexpected status: %s', event.status) @@ -295,6 +303,13 @@ export class TestRunner implements vscode.Disposable { } } + /** + * Checks if the current test run is for loading tests rather than running them + */ + private isTestLoad(context: TestRunContext): boolean { + return context.request.profile!.label == 'ResolveTests' + } + private killProfileTestRun(context: TestRunContext) { let profileKind = context.request.profile!.kind let process = this.testProcessMap.get(profileKind) From 985d72a4569d294a5cd1a9a22105a35f40f8c4c5 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Fri, 13 Jan 2023 13:46:44 +0000 Subject: [PATCH 076/108] Remove commented out method for sorting tests --- src/testRunner.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index 04bcb41..67acf68 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -68,26 +68,6 @@ export class TestRunner implements vscode.Disposable { return parseInt(test.id.substring(test.id.indexOf("[") + 1, test.id.lastIndexOf("]")).split(':').join('')); } - // /** - // * Sorts an array of TestSuiteInfo objects by label. - // * - // * @param testSuiteChildren An array of TestSuiteInfo objects, generally the children of another TestSuiteInfo object. - // * @return The input array, sorted by label. - // */ - // protected sortTestSuiteChildren(testSuiteChildren: Array): Array { - // testSuiteChildren = testSuiteChildren.sort((a: TestSuiteInfo, b: TestSuiteInfo) => { - // let comparison = 0; - // if (a.label > b.label) { - // comparison = 1; - // } else if (a.label < b.label) { - // comparison = -1; - // } - // return comparison; - // }); - - // return testSuiteChildren; - // } - /** * Test run handler * From d0e84f12503e2b81aac8f4fd9337ab4953215b28 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 14 Jan 2023 18:29:15 +0000 Subject: [PATCH 077/108] Use VSC stdout logger in tests, delete stub logger and some unused mocks --- src/frameworkProcess.ts | 2 +- src/testLoader.ts | 2 +- test/stubs/logger.ts | 109 ------------------ test/stubs/stubTestItemCollection.ts | 6 +- test/suite/helpers.ts | 50 +++----- test/suite/minitest/minitest.test.ts | 12 +- test/suite/rspec/rspec.test.ts | 12 +- test/suite/unitTests/config.test.ts | 2 +- test/suite/unitTests/frameworkProcess.test.ts | 3 +- test/suite/unitTests/testSuiteManager.test.ts | 3 +- 10 files changed, 43 insertions(+), 158 deletions(-) delete mode 100644 test/stubs/logger.ts diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 3e16b07..faf10ad 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -136,7 +136,7 @@ export class FrameworkProcess implements vscode.Disposable { // it should always have a uri, but just to be safe... testMessage.location = new vscode.Location(testItem.uri, testItem.range) } else { - log.error('Test missing location details', testItem.id, testItem.uri) + log.error('Test missing location details', { testId: testItem.id, uri: testItem.uri }) } } this.testStatusEmitter.fire(new TestStatus( diff --git a/src/testLoader.ts b/src/testLoader.ts index 1e045eb..8741a8d 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -119,7 +119,7 @@ export class TestLoader implements vscode.Disposable { */ private async loadTests(testItems?: vscode.TestItem[]): Promise { let log = this.log.getChildLogger({label:'loadTests'}) - log.info('Loading tests...', testItems?.map(x => x.id) || 'all tests'); + log.info('Loading tests...', { testIds: testItems?.map(x => x.id) || 'all tests' }); try { if (testItems) { for (const item of testItems) { item.busy = true }} let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) diff --git a/test/stubs/logger.ts b/test/stubs/logger.ts deleted file mode 100644 index 754ea8a..0000000 --- a/test/stubs/logger.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { LogLevel } from "@vscode-logging/logger"; -import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; - -function createChildLogger(parent: IVSCodeExtLogger, label: string): IChildLogger { - let prependLabel = (l:string, m:string):string => `${l}: ${m}` - return { - ...parent, - debug: (msg: string, ...args: any[]) => { parent.debug(prependLabel(label, msg), ...args) }, - error: (msg: string, ...args: any[]) => { parent.error(prependLabel(label, msg), ...args) }, - fatal: (msg: string, ...args: any[]) => { parent.fatal(prependLabel(label, msg), ...args) }, - info: (msg: string, ...args: any[]) => { parent.info(prependLabel(label, msg), ...args) }, - trace: (msg: string, ...args: any[]) => { parent.trace(prependLabel(label, msg), ...args) }, - warn: (msg: string, ...args: any[]) => { parent.warn(prependLabel(label, msg), ...args) } - } -} - -function noop() {} - -/** - * Noop logger for use in testing where logs are usually unnecessary - */ -export const NOOP_LOGGER: IVSCodeExtLogger = { - changeLevel: noop, - changeSourceLocationTracking: noop, - debug: noop, - error: noop, - fatal: noop, - getChildLogger(opts: { label: string }): IChildLogger { - return this; - }, - info: noop, - trace: noop, - warn: noop -}; -Object.freeze(NOOP_LOGGER); - -function stdout_logger(level: LogLevel = "info"): IVSCodeExtLogger { - const levels: { [key: string]: number } = { - "fatal": 0, - "error": 1, - "warn": 2, - "info": 3, - "debug": 4, - "trace": 5, - } - const divider = '----------' - let maxLevel = levels[level] - function writeStdOutLogMsg(level: LogLevel, msg: string, ...args: any[]): void { - if (levels[level] <= maxLevel) { - let message = `[${level}] ${msg}${args.length > 0 ? ':' : ''}` - args.forEach((arg) => { - if (arg instanceof Error) { - message = `${message}\n ${arg.stack ? arg.stack : arg.name + ': ' + arg.message}` - } else { - message = `${message}\n ${JSON.stringify(arg)}` - } - }) - switch(level) { - case "fatal": - case "error": - console.error(message) - console.error(divider) - break; - case "warn": - console.warn(message) - console.warn(divider) - break; - case "info": - console.info(message) - console.info(divider) - break; - case "debug": - case "trace": - console.debug(message) - console.debug(divider) - break; - } - } - } - let logger: IVSCodeExtLogger = { - changeLevel: (level: LogLevel) => { maxLevel = levels[level] }, - changeSourceLocationTracking: noop, - debug: (msg: string, ...args: any[]) => { writeStdOutLogMsg("debug", msg, ...args) }, - error: (msg: string, ...args: any[]) => { writeStdOutLogMsg("error", msg, ...args) }, - fatal: (msg: string, ...args: any[]) => { writeStdOutLogMsg("fatal", msg, ...args) }, - getChildLogger(opts: { label: string }): IChildLogger { - return createChildLogger(this, opts.label); - }, - info: (msg: string, ...args: any[]) => { writeStdOutLogMsg("info", msg, ...args) }, - trace: (msg: string, ...args: any[]) => { writeStdOutLogMsg("trace", msg, ...args) }, - warn: (msg: string, ...args: any[]) => { writeStdOutLogMsg("warn", msg, ...args) } - } - return logger -} - -/** - * Get a logger - * - * @param level One of "off", "fatal", "error", "warn", "info", "debug", "trace" - * @returns a noop logger if level is "off", else a logger that logs to stdout at the specified level and below - * (e.g. logger("warn") would return a logger that logs only messages logged at "fatal", "error", and "warn" levels) - */ -export function logger(level: LogLevel = "info"): IVSCodeExtLogger { - if (level == "off") { - return NOOP_LOGGER - } else { - return stdout_logger(level) - } -} diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts index 491cf45..661dc85 100644 --- a/test/stubs/stubTestItemCollection.ts +++ b/test/stubs/stubTestItemCollection.ts @@ -14,7 +14,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } replace(items: readonly vscode.TestItem[]): void { - this.log.debug(`Replacing all tests`, JSON.stringify(Object.keys(this.testIds)), JSON.stringify(items.map(x => x.id))) + this.log.debug(`Replacing all tests`, { currentItems: JSON.stringify(Object.keys(this.testIds)), newItems: JSON.stringify(items.map(x => x.id)) }) this.testIds = {} items.forEach(item => { this.testIds[item.id] = item @@ -47,7 +47,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } add(item: vscode.TestItem): void { - this.log.debug('Adding test to collection', item.id, Object.keys(this.testIds)) + this.log.debug('Adding test to collection', { testId: item.id, collectionItems: Object.keys(this.testIds) }) this.testIds[item.id] = item let sortedIds = Object.values(this.testIds).sort((a, b) => { if(a.id > b.id) return 1 @@ -62,7 +62,7 @@ export class StubTestItemCollection implements vscode.TestItemCollection { } delete(itemId: string): void { - this.log.debug('Deleting test from collection', itemId, Object.keys(this.testIds)) + this.log.debug('Deleting test from collection', { testId: itemId, collectionItems: Object.keys(this.testIds) }) delete this.testIds[itemId] } diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index eb1b4ba..febc3ba 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,13 +1,24 @@ import * as vscode from 'vscode' import { expect } from 'chai' -import { IChildLogger } from "@vscode-logging/types"; -import { anyString, anything, capture, instance, mock, when } from 'ts-mockito'; +import { capture, instance, mock, when } from 'ts-mockito'; import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; -import { NOOP_LOGGER } from '../stubs/logger'; -import { StubTestItemCollection } from '../stubs/stubTestItemCollection'; import { TestSuiteManager } from '../testSuiteManager'; +import { getExtensionLogger, IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; +/** + * Get a logger + * + * @param level One of "off", "fatal", "error", "warn", "info", "debug", "trace" + * @returns a logger that logs to stdout at the specified level + */ +export function logger(level: LogLevel = "info"): IVSCodeExtLogger { + return getExtensionLogger({ + extName: "RubyTestExplorer - Tests", + level: (level as LogLevel), + logConsole: true + }) +} /** * Object to simplify describing a {@link vscode.TestItem TestItem} for testing its values @@ -125,29 +136,6 @@ export function verifyFailure( expect(failureDetails.location?.uri.fsPath).to.eq(expectedTestItem.file, `${messagePrefix}: path`) } -export function setupMockTestController(rootLog?: IChildLogger): vscode.TestController { - let mockTestController = mock() - let createTestItem = (id: string, label: string, uri?: vscode.Uri | undefined) => { - return { - id: id, - label: label, - uri: uri, - canResolveChildren: false, - parent: undefined, - tags: [], - busy: false, - range: undefined, - error: undefined, - children: new StubTestItemCollection(rootLog || NOOP_LOGGER, instance(mockTestController)), - } - } - when(mockTestController.createTestItem(anyString(), anyString())).thenCall(createTestItem) - when(mockTestController.createTestItem(anyString(), anyString(), anything())).thenCall(createTestItem) - let testItems = new StubTestItemCollection(rootLog || NOOP_LOGGER, instance(mockTestController)) - when(mockTestController.items).thenReturn(testItems) - return mockTestController -} - export function setupMockRequest(manager: TestSuiteManager, testId?: string | string[]): vscode.TestRunRequest { let mockRequest = mock() if (testId) { @@ -173,14 +161,6 @@ export function setupMockRequest(manager: TestSuiteManager, testId?: string | st return mockRequest } -export function getMockCancellationToken(): vscode.CancellationToken { - let mockToken = mock() - when(mockToken.isCancellationRequested).thenReturn(false) - when(mockToken.onCancellationRequested(anything(), anything(), undefined)).thenReturn({ dispose: () => {} }) - when(mockToken.onCancellationRequested(anything(), anything(), anything())).thenReturn({ dispose: () => {} }) - return instance(mockToken) -} - /** * Argument captors for test state reporting functions * diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 4fa8956..0d89e55 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -9,8 +9,16 @@ import { TestSuiteManager } from '../../../src/testSuiteManager'; import { TestRunner } from '../../../src/testRunner'; import { MinitestConfig } from '../../../src/minitest/minitestConfig'; -import { setupMockRequest, TestFailureExpectation, testItemCollectionMatches, TestItemExpectation, testItemMatches, testStateCaptors, verifyFailure } from '../helpers'; -import { logger } from '../..//stubs/logger'; +import { + logger, + setupMockRequest, + TestFailureExpectation, + testItemCollectionMatches, + TestItemExpectation, + testItemMatches, + testStateCaptors, + verifyFailure +} from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 5d79440..7bd127b 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -9,8 +9,16 @@ import { TestSuiteManager } from '../../../src/testSuiteManager'; import { TestRunner } from '../../../src/testRunner'; import { RspecConfig } from '../../../src/rspec/rspecConfig'; -import { setupMockRequest, testItemCollectionMatches, testItemMatches, testStateCaptors, verifyFailure, TestItemExpectation, TestFailureExpectation } from '../helpers'; -import { logger } from '../../stubs/logger'; +import { + logger, + setupMockRequest, + testItemCollectionMatches, + testItemMatches, + testStateCaptors, + verifyFailure, + TestItemExpectation, + TestFailureExpectation +} from '../helpers'; import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index af0cddf..0ab5443 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -5,7 +5,7 @@ import * as path from 'path' import { Config } from "../../../src/config"; import { RspecConfig } from "../../../src/rspec/rspecConfig"; -import { logger } from "../../stubs/logger"; +import { logger } from "../helpers"; const log = logger("off") diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index fa4fc1a..02c65f6 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -9,8 +9,7 @@ import { TestSuiteManager } from "../../../src/testSuiteManager"; import { TestRunContext } from '../../../src/testRunContext'; import { FrameworkProcess } from '../../../src/frameworkProcess'; -import { testItemCollectionMatches, TestItemExpectation } from "../helpers"; -import { logger } from '../../stubs/logger'; +import { logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; // JSON Fixtures import rspecDryRunOutput from '../../fixtures/unitTests/rspec/dryRunOutput.json' diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts index f240f06..a03809f 100644 --- a/test/suite/unitTests/testSuiteManager.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -6,8 +6,7 @@ import path from 'path' import { Config } from '../../../src/config'; import { TestSuiteManager } from '../../../src/testSuiteManager'; -import { logger } from '../../stubs/logger'; -import { testUriMatches } from '../helpers'; +import { logger, testUriMatches } from '../helpers'; const log = logger("off") From c26f9fe2a60f6379a4f35f5b147ce55fac122c9d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sat, 14 Jan 2023 20:07:51 +0000 Subject: [PATCH 078/108] Refactor TestRunContext into TestStatusListener to simplify things --- src/frameworkProcess.ts | 5 +- src/testRunContext.ts | 154 ++++++------------ src/testRunner.ts | 111 +++++-------- test/stubs/stubTestController.ts | 4 +- test/suite/rspec/rspec.test.ts | 2 +- test/suite/unitTests/frameworkProcess.test.ts | 13 +- 6 files changed, 100 insertions(+), 189 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index faf10ad..b979812 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -4,7 +4,6 @@ import * as vscode from 'vscode'; import split2 from 'split2'; import { IChildLogger } from '@vscode-logging/logger'; import { Status, TestStatus } from './testStatus'; -import { TestRunContext } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; type ParsedTest = { @@ -38,11 +37,11 @@ export class FrameworkProcess implements vscode.Disposable { readonly rootLog: IChildLogger, private readonly testCommand: string, private readonly spawnArgs: childProcess.SpawnOptions, - private readonly testContext: TestRunContext, + private readonly cancellationToken: vscode.CancellationToken, private readonly testManager: TestSuiteManager, ) { this.log = rootLog.getChildLogger({label: 'FrameworkProcess'}) - this.disposables.push(this.testContext.cancellationToken.onCancellationRequested(() => { + this.disposables.push(this.cancellationToken.onCancellationRequested(() => { this.log.debug('Cancellation requested') this.dispose() })) diff --git a/src/testRunContext.ts b/src/testRunContext.ts index 4325c7d..caeb35d 100644 --- a/src/testRunContext.ts +++ b/src/testRunContext.ts @@ -1,117 +1,59 @@ import * as vscode from 'vscode' import { IChildLogger } from '@vscode-logging/logger' +import { Status, TestStatus } from './testStatus' /** - * Test run context - * - * Contains all objects used for interacting with VS Test API while tests are running + * Updates the TestRun when test status events are received */ -export class TestRunContext { - public readonly testRun: vscode.TestRun - public readonly log: IChildLogger - - /** - * Create a new context - * - * @param log Logger - * @param cancellationToken Cancellation token triggered when the user cancels a test operation - * @param request Test run request for creating test run object - * @param controller Test controller to look up tests for status reporting - * @param debuggerConfig A VS Code debugger configuration. - */ - constructor( - readonly rootLog: IChildLogger, - public readonly cancellationToken: vscode.CancellationToken, - readonly request: vscode.TestRunRequest, - readonly controller: vscode.TestController, - public readonly debuggerConfig?: vscode.DebugConfiguration - ) { - this.log = rootLog.getChildLogger({ label: `${TestRunContext.name}(${request.profile?.label})` }) - this.log.info('Creating test run', {request: request}); - this.testRun = controller.createTestRun(request) - } - - /** - * Indicates a test is queued for later execution. - * - * @param test Test item to update. - */ - public enqueued(test: vscode.TestItem): void { - this.log.debug('Enqueued test: %s', test.id) - this.testRun.enqueued(test) +export class TestStatusListener { + public static listen( + rootLog: IChildLogger, + profile: vscode.TestRunProfile, + testRun: vscode.TestRun, + testStatusEmitter: vscode.EventEmitter + ): vscode.Disposable { + let log = rootLog.getChildLogger({ label: `${TestStatusListener.name}(${profile.label})`}) + return testStatusEmitter.event((event: TestStatus) => { + + switch(event.status) { + case Status.skipped: + log.debug('Received test skipped event: %s', event.testItem.id) + testRun.skipped(event.testItem) + break; + case Status.passed: + if (this.isTestLoad(profile)) { + log.debug('Ignored test passed event from test load: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) + testRun.passed(event.testItem, event.duration) + } + break; + case Status.errored: + log.debug('Received test errored event: %s (duration: %d)', event.testItem.id, event.duration, event.message) + testRun.errored(event.testItem, event.message!, event.duration) + break; + case Status.failed: + log.debug('Received test failed event: %s (duration: %d)', event.testItem.id, event.duration, event.message) + testRun.failed(event.testItem, event.message!, event.duration) + break; + case Status.running: + if (this.isTestLoad(profile)) { + log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) + } else { + log.debug('Received test started event: %s', event.testItem.id) + testRun.started(event.testItem) + } + break; + default: + log.warn('Unexpected status: %s', event.status) + } + }) } /** - * Indicates a test has errored. - * - * This differs from the "failed" state in that it indicates a test that couldn't be executed at all, from a compilation error for example - * - * @param test Test item to update. - * @param message Message(s) associated with the test failure. - * @param duration How long the test took to execute, in milliseconds. + * Checks if the current test run is for loading tests rather than running them */ - public errored( - test: vscode.TestItem, - message: vscode.TestMessage, - duration?: number - ): void { - this.log.debug('Errored test: %s (duration: %d)', test.id, duration, message) - this.testRun.errored(test, message, duration) - } - - /** - * Indicates a test has failed. - * - * @param test Test item to update. - * @param message Message(s) associated with the test failure. - * @param file Path to the file containing the failed test - * @param line Line number where the error occurred - * @param duration How long the test took to execute, in milliseconds. - */ - public failed( - test: vscode.TestItem, - message: vscode.TestMessage, - duration?: number - ): void { - this.log.debug('Failed test: %s (duration: %d)', test.id, duration, message) - this.testRun.failed(test, message, duration) - } - - /** - * Indicates a test has passed. - * - * @param test Test item to update. - * @param duration How long the test took to execute, in milliseconds. - */ - public passed(test: vscode.TestItem, - duration?: number | undefined - ): void { - this.log.debug('Passed test: %s (duration: %d)', test.id, duration) - this.testRun.passed(test, duration) - } - - /** - * Indicates a test has been skipped. - * - * @param test ID of the test item to update, or the test item. - */ - public skipped(test: vscode.TestItem): void { - this.log.debug('Skipped test: %s', test.id) - this.testRun.skipped(test) - } - - /** - * Indicates a test has started running. - * - * @param test Test item to update, or the test item. - */ - public started(test: vscode.TestItem): void { - this.log.debug('Started test: %s', test.id) - this.testRun.started(test) - } - - public endTestRun(): void { - this.log.info('Ending test run'); - this.testRun.end() + private static isTestLoad(profile: vscode.TestRunProfile): boolean { + return profile.label == 'ResolveTests' } } diff --git a/src/testRunner.ts b/src/testRunner.ts index 67acf68..997e3c4 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -2,10 +2,9 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; -import { TestRunContext } from './testRunContext'; +import { TestStatusListener } from './testRunContext'; import { TestSuiteManager } from './testSuiteManager'; import { FrameworkProcess } from './frameworkProcess'; -import { Status, TestStatus } from './testStatus'; export class TestRunner implements vscode.Disposable { protected debugCommandStartedResolver?: () => void; @@ -83,14 +82,16 @@ export class TestRunner implements vscode.Disposable { ) { let log = this.log.getChildLogger({ label: 'runHandler' }) + if (!request.profile) { + log.error('Test run request is missing a profile', {request: request}) + return + } + // Loop through all included tests, or all known tests, and add them to our queue log.debug('Number of tests in request', request.include?.length || 0); - let context = new TestRunContext( - this.rootLog, - token, - request, - this.manager.controller - ); + + log.info('Creating test run', {request: request}); + const testRun = this.manager.controller.createTestRun(request) try { log.trace("Included tests in request", request.include?.map(x => x.id)); @@ -99,26 +100,26 @@ export class TestRunner implements vscode.Disposable { log.trace("Running tests", testsToRun?.map(x => x.id)); let command: string - if (context.request.profile?.label === 'ResolveTests') { + if (request.profile.label === 'ResolveTests') { // Load tests command = this.manager.config.getResolveTestsCommand(testsToRun) - await this.runTestFramework(command, context) + await this.runTestFramework(command, testRun, request.profile) } else { // Run tests if (!testsToRun) { log.debug("Running all tests") this.manager.controller.items.forEach((item) => { // Mark selected tests as started - this.enqueTestAndChildren(item, context) + this.enqueTestAndChildren(item, testRun) }) - command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.manager.config.getFullTestSuiteCommand(debuggerConfig) } else { log.debug("Running selected tests") - command = this.manager.config.getFullTestSuiteCommand(context.debuggerConfig) + command = this.manager.config.getFullTestSuiteCommand(debuggerConfig) for (const node of testsToRun) { log.trace('Adding test to command: %s', node.id) // Mark selected tests as started - this.enqueTestAndChildren(node, context) + this.enqueTestAndChildren(node, testRun) command = `${command} ${node.uri?.fsPath}` if (!node.canResolveChildren) { // single test @@ -132,11 +133,14 @@ export class TestRunner implements vscode.Disposable { } if (debuggerConfig) { log.debug('Debugging tests', request.include?.map(x => x.id)); - await Promise.all([this.startDebugSession(debuggerConfig, context), this.runTestFramework(command, context)]) + await Promise.all([ + this.startDebugSession(debuggerConfig, request.profile), + this.runTestFramework(command, testRun, request.profile) + ]) } else { log.debug('Running test', request.include?.map(x => x.id)); - await this.runTestFramework(command, context) + await this.runTestFramework(command, testRun, request.profile) } } } @@ -146,14 +150,14 @@ export class TestRunner implements vscode.Disposable { finally { // Make sure to end the run after all tests have been executed: log.debug('Ending test run'); - context.endTestRun(); + testRun.end(); } if (token.isCancellationRequested) { log.info('Test run aborted due to cancellation') } } - private async startDebugSession(debuggerConfig: vscode.DebugConfiguration, context: TestRunContext): Promise { + private async startDebugSession(debuggerConfig: vscode.DebugConfiguration, profile: vscode.TestRunProfile): Promise { let log = this.log.getChildLogger({label: 'startDebugSession'}) if (this.workspace) { @@ -194,24 +198,24 @@ export class TestRunner implements vscode.Disposable { const debugStopSubscription = vscode.debug.onDidTerminateDebugSession(session => { if (session === activeDebugSession) { log.info('Debug session ended', session.name); - this.killProfileTestRun(context) // terminate the test run + this.killTestRun(profile) // terminate the test run debugStopSubscription.dispose(); } }) } catch (err) { log.error('Error starting debug session', err) - this.killProfileTestRun(context) + this.killTestRun(profile) } } /** * Mark a test node and all its children as being queued for execution */ - private enqueTestAndChildren(test: vscode.TestItem, context: TestRunContext) { + private enqueTestAndChildren(test: vscode.TestItem, testRun: vscode.TestRun) { // Tests will be marked as started as the runner gets to them - context.enqueued(test); + testRun.enqueued(test); if (test.children && test.children.size > 0) { - test.children.forEach(child => { this.enqueTestAndChildren(child, context) }) + test.children.forEach(child => { this.enqueTestAndChildren(child, testRun) }) } } @@ -223,7 +227,7 @@ export class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - private async runTestFramework (testCommand: string, context: TestRunContext): Promise { + private async runTestFramework (testCommand: string, testRun: vscode.TestRun, profile: vscode.TestRunProfile): Promise { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, @@ -231,67 +235,38 @@ export class TestRunner implements vscode.Disposable { }; this.log.info('Running command: %s', testCommand); - let testProfileKind = context.request.profile!.kind + let testProfileKind = profile.kind if (this.testProcessMap.get(testProfileKind)) { this.log.warn('Test run already in progress for profile kind: %s', testProfileKind) return } - let testProcess = new FrameworkProcess(this.log, testCommand, spawnArgs, context, this.manager) + let testProcess = new FrameworkProcess(this.log, testCommand, spawnArgs, testRun.token, this.manager) this.disposables.push(testProcess) this.testProcessMap.set(testProfileKind, testProcess); - testProcess.testStatusEmitter.event((event: TestStatus) => { - let log = this.log.getChildLogger({label: 'testStatusListener'}) - switch(event.status) { - case Status.skipped: - log.debug('Received test skipped event: %s', event.testItem.id) - context.skipped(event.testItem) - break; - case Status.passed: - if (this.isTestLoad(context)) { - log.debug('Ignored test passed event from test load: %s (duration: %d)', event.testItem.id, event.duration) - } else { - log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) - context.passed(event.testItem, event.duration) - } - break; - case Status.errored: - log.debug('Received test errored event: %s (duration: %d)', event.testItem.id, event.duration, event.message) - context.errored(event.testItem, event.message!, event.duration) - break; - case Status.failed: - log.debug('Received test failed event: %s (duration: %d)', event.testItem.id, event.duration, event.message) - context.failed(event.testItem, event.message!, event.duration) - break; - case Status.running: - if (this.isTestLoad(context)) { - log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) - } else { - log.debug('Received test started event: %s', event.testItem.id) - context.started(event.testItem) - } - break; - default: - log.warn('Unexpected status: %s', event.status) - } - }) + const statusListener = TestStatusListener.listen( + this.rootLog, + profile, + testRun, + testProcess.testStatusEmitter + ) + this.disposables.push(statusListener) try { await testProcess.startProcess(this.debugCommandStartedResolver) } finally { + this.disposeInstance(statusListener) + this.disposeInstance(testProcess) this.testProcessMap.delete(testProfileKind) } } /** - * Checks if the current test run is for loading tests rather than running them + * Terminates the current test run process for the given profile kind if there is one + * @param profile The profile to kill the test run for */ - private isTestLoad(context: TestRunContext): boolean { - return context.request.profile!.label == 'ResolveTests' - } - - private killProfileTestRun(context: TestRunContext) { - let profileKind = context.request.profile!.kind + private killTestRun(profile: vscode.TestRunProfile) { + let profileKind = profile.kind let process = this.testProcessMap.get(profileKind) if (process) { this.disposeInstance(process) diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts index a9ed562..c2a4823 100644 --- a/test/stubs/stubTestController.ts +++ b/test/stubs/stubTestController.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import { instance, mock } from 'ts-mockito'; +import { instance, mock, when } from 'ts-mockito'; import { StubTestItemCollection } from './stubTestItemCollection'; import { StubTestItem } from './stubTestItem'; @@ -11,6 +11,7 @@ export class StubTestController implements vscode.TestController { items: vscode.TestItemCollection testRuns: Map = new Map() readonly rootLog: IChildLogger + readonly cancellationTokenSource = new vscode.CancellationTokenSource() constructor(readonly log: IChildLogger) { this.rootLog = log @@ -38,6 +39,7 @@ export class StubTestController implements vscode.TestController { ): vscode.TestRun { let mockTestRun = mock() this.testRuns.set(request.profile!.label, mockTestRun) + when(mockTestRun.token).thenReturn(this.cancellationTokenSource.token) return instance(mockTestRun) } diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 7bd127b..36f1cf8 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -30,7 +30,7 @@ suite('Extension Test for RSpec', function() { let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; - const log = logger("info"); + const log = logger("debug"); let expectedPath = (file: string): string => { return path.resolve( diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 02c65f6..47c82aa 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -6,7 +6,6 @@ import * as path from 'path' import { Config } from "../../../src/config"; import { TestSuiteManager } from "../../../src/testSuiteManager"; -import { TestRunContext } from '../../../src/testRunContext'; import { FrameworkProcess } from '../../../src/frameworkProcess'; import { logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; @@ -18,22 +17,16 @@ import minitestDryRunOutput from '../../fixtures/unitTests/minitest/dryRunOutput import minitestTestRunOutput from '../../fixtures/unitTests/minitest/testRunOutput.json' const log = logger("off") -const cancellationTokenSoure = new vscode.CancellationTokenSource() +const cancellationTokenSource = new vscode.CancellationTokenSource() suite('FrameworkProcess', function () { let manager: TestSuiteManager let testController: vscode.TestController - let mockContext: TestRunContext let frameworkProcess: FrameworkProcess let spawnOptions: childProcess.SpawnOptions = {} const config = mock() - before(function () { - mockContext = mock() - when(mockContext.cancellationToken).thenReturn(cancellationTokenSoure.token) - }) - afterEach(function() { if (testController) { testController.dispose() @@ -51,7 +44,7 @@ suite('FrameworkProcess', function () { beforeEach(function () { testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) - frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) }) const expectedTests: TestItemExpectation[] = [ @@ -241,7 +234,7 @@ suite('FrameworkProcess', function () { beforeEach(function () { testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) - frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, instance(mockContext), manager) + frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) }) From 6abc51d25dba9262c51b27d7f20e9462e9a16c56 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 12:06:14 +0000 Subject: [PATCH 079/108] Delete stub implementations used in tests Tests are now using the actual implementations from VSC --- package-lock.json | 38 ++++----- package.json | 2 +- src/testRunner.ts | 2 + test/stubs/stubTestController.ts | 56 ------------- test/stubs/stubTestItem.ts | 28 ------- test/stubs/stubTestItemCollection.ts | 81 ------------------- test/suite/helpers.ts | 4 +- test/suite/minitest/minitest.test.ts | 49 ++++++----- test/suite/rspec/rspec.test.ts | 50 +++++++----- test/suite/unitTests/config.test.ts | 2 +- test/suite/unitTests/frameworkProcess.test.ts | 6 +- test/suite/unitTests/testSuiteManager.test.ts | 4 +- 12 files changed, 88 insertions(+), 234 deletions(-) delete mode 100644 test/stubs/stubTestController.ts delete mode 100644 test/stubs/stubTestItem.ts delete mode 100644 test/stubs/stubTestItemCollection.ts diff --git a/package-lock.json b/package-lock.json index a23b3e7..9bace9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,13 @@ "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", + "@typestrong/ts-mockito": "^2.6.4", "@vscode/test-electron": "^2.1.2", "@vscode/vsce": "^2.16.0", "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", - "ts-mockito": "^2.6.1", "typescript": "^4.7.4" }, "engines": { @@ -109,6 +109,15 @@ "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", "dev": true }, + "node_modules/@typestrong/ts-mockito": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@typestrong/ts-mockito/-/ts-mockito-2.6.4.tgz", + "integrity": "sha512-IMUau44ixvAbO7ylg/NJAwtGoAIcC5SwAm1b+QcZWMMTqw0n2NZn4nUkMWZfUY478whZ50vvMiu7rJJfCR9C9A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -2740,15 +2749,6 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, - "node_modules/ts-mockito": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", - "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.5" - } - }, "node_modules/ts-mutex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", @@ -3149,6 +3149,15 @@ "integrity": "sha512-3/9Fz0F2eBgwciazc94Ien+9u1elnjFg9YAhvAb3qDy/WeFWD9VrOPU7CIytryOVUdbxus8uzL4VZYONA0gDtA==", "dev": true }, + "@typestrong/ts-mockito": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@typestrong/ts-mockito/-/ts-mockito-2.6.4.tgz", + "integrity": "sha512-IMUau44ixvAbO7ylg/NJAwtGoAIcC5SwAm1b+QcZWMMTqw0n2NZn4nUkMWZfUY478whZ50vvMiu7rJJfCR9C9A==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -5157,15 +5166,6 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, - "ts-mockito": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", - "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", - "dev": true, - "requires": { - "lodash": "^4.17.5" - } - }, "ts-mutex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ts-mutex/-/ts-mutex-1.0.0.tgz", diff --git a/package.json b/package.json index 4a2f161..2d31dd4 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,13 @@ "@types/mocha": "^9.1.0", "@types/split2": "^3.2.1", "@types/vscode": "^1.69.0", + "@typestrong/ts-mockito": "^2.6.4", "@vscode/test-electron": "^2.1.2", "@vscode/vsce": "^2.16.0", "chai": "^4.3.6", "glob": "^8.0.3", "mocha": "^9.2.2", "rimraf": "^3.0.0", - "ts-mockito": "^2.6.1", "typescript": "^4.7.4" }, "engines": { diff --git a/src/testRunner.ts b/src/testRunner.ts index 997e3c4..1abb840 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -213,6 +213,8 @@ export class TestRunner implements vscode.Disposable { */ private enqueTestAndChildren(test: vscode.TestItem, testRun: vscode.TestRun) { // Tests will be marked as started as the runner gets to them + let log = this.log.getChildLogger({label: `${this.enqueTestAndChildren.name}`}) + log.debug('Enqueueing test item: %s', test.id) testRun.enqueued(test); if (test.children && test.children.size > 0) { test.children.forEach(child => { this.enqueTestAndChildren(child, testRun) }) diff --git a/test/stubs/stubTestController.ts b/test/stubs/stubTestController.ts deleted file mode 100644 index c2a4823..0000000 --- a/test/stubs/stubTestController.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as vscode from 'vscode' -import { instance, mock, when } from 'ts-mockito'; - -import { StubTestItemCollection } from './stubTestItemCollection'; -import { StubTestItem } from './stubTestItem'; -import { IChildLogger } from '@vscode-logging/logger'; - -export class StubTestController implements vscode.TestController { - id: string = "stub_test_controller_id"; - label: string = "stub_test_controller_label"; - items: vscode.TestItemCollection - testRuns: Map = new Map() - readonly rootLog: IChildLogger - readonly cancellationTokenSource = new vscode.CancellationTokenSource() - - constructor(readonly log: IChildLogger) { - this.rootLog = log - this.items = new StubTestItemCollection(log, this); - } - - createRunProfile( - label: string, - kind: vscode.TestRunProfileKind, - runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => void | Thenable, - isDefault?: boolean, - tag?: vscode.TestTag - ): vscode.TestRunProfile { - return instance(mock()) - } - - resolveHandler?: ((item: vscode.TestItem | undefined) => void | Thenable) | undefined; - - refreshHandler: ((token: vscode.CancellationToken) => void | Thenable) | undefined; - - createTestRun( - request: vscode.TestRunRequest, - name?: string, - persist?: boolean - ): vscode.TestRun { - let mockTestRun = mock() - this.testRuns.set(request.profile!.label, mockTestRun) - when(mockTestRun.token).thenReturn(this.cancellationTokenSource.token) - return instance(mockTestRun) - } - - createTestItem(id: string, label: string, uri?: vscode.Uri): vscode.TestItem { - return new StubTestItem(this.rootLog, this, id, label, uri) - } - - getMockTestRun(request: vscode.TestRunRequest): vscode.TestRun | undefined { - return this.testRuns.get(request.profile!.label) - } - - dispose = () => {} - -} \ No newline at end of file diff --git a/test/stubs/stubTestItem.ts b/test/stubs/stubTestItem.ts deleted file mode 100644 index c6205e0..0000000 --- a/test/stubs/stubTestItem.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IChildLogger } from '@vscode-logging/logger'; -import * as vscode from 'vscode' - -import { StubTestItemCollection } from './stubTestItemCollection'; - -export class StubTestItem implements vscode.TestItem { - id: string; - uri: vscode.Uri | undefined; - children: vscode.TestItemCollection; - parent: vscode.TestItem | undefined; - tags: readonly vscode.TestTag[]; - canResolveChildren: boolean; - busy: boolean; - label: string; - description: string | undefined; - range: vscode.Range | undefined; - error: string | vscode.MarkdownString | undefined; - - constructor(rootLog: IChildLogger, controller: vscode.TestController, id: string, label: string, uri?: vscode.Uri) { - this.id = id - this.label = label - this.uri = uri - this.children = new StubTestItemCollection(rootLog, controller, this) - this.tags = [] - this.canResolveChildren = false - this.busy = false - } -} \ No newline at end of file diff --git a/test/stubs/stubTestItemCollection.ts b/test/stubs/stubTestItemCollection.ts deleted file mode 100644 index 661dc85..0000000 --- a/test/stubs/stubTestItemCollection.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { IChildLogger } from '@vscode-logging/logger'; -import * as vscode from 'vscode' -import { StubTestItem } from './stubTestItem'; - -export class StubTestItemCollection implements vscode.TestItemCollection { - private testIds: { [name: string]: vscode.TestItem } = {}; - get size(): number { return Object.keys(this.testIds).length; }; - private readonly log: IChildLogger - private readonly parentItem?: vscode.TestItem; - - constructor(readonly rootLog: IChildLogger, controller: vscode.TestController, parent?: vscode.TestItem) { - this.log = rootLog.getChildLogger({label: `StubTestItemCollection(${parent?.id || 'controller'})`}) - this.parentItem = parent - } - - replace(items: readonly vscode.TestItem[]): void { - this.log.debug(`Replacing all tests`, { currentItems: JSON.stringify(Object.keys(this.testIds)), newItems: JSON.stringify(items.map(x => x.id)) }) - this.testIds = {} - items.forEach(item => { - this.testIds[item.id] = item - }) - } - - forEach(callback: (item: vscode.TestItem, collection: vscode.TestItemCollection) => unknown, thisArg?: unknown): void { - Object.values(this.testIds).forEach((element: vscode.TestItem) => { - return callback(element, this) - }); - } - - [Symbol.iterator](): Iterator<[id: string, testItem: vscode.TestItem]> { - let step = 0; - const iterator = { - next(): IteratorResult<[id: string, testItem: vscode.TestItem]> { - let testId = Object.keys(super.testIds)[step]; - let value: [id: string, testItem: vscode.TestItem] = [ - testId, - super.testIds[testId] - ]; - step++; - return { - value: value, - done: step >= super.size - } - } - } - return iterator; - } - - add(item: vscode.TestItem): void { - this.log.debug('Adding test to collection', { testId: item.id, collectionItems: Object.keys(this.testIds) }) - this.testIds[item.id] = item - let sortedIds = Object.values(this.testIds).sort((a, b) => { - if(a.id > b.id) return 1 - if(a.id < b.id) return -1 - return 0 - }) - this.testIds = {} - sortedIds.forEach(item => this.testIds[item.id] = item) - if (this.parentItem) { - (item as StubTestItem).parent = this.parentItem - } - } - - delete(itemId: string): void { - this.log.debug('Deleting test from collection', { testId: itemId, collectionItems: Object.keys(this.testIds) }) - delete this.testIds[itemId] - } - - get(itemId: string): vscode.TestItem | undefined { - return this.testIds[itemId] - } - - toString(): string { - var output = [] - output.push("[") - this.forEach((item, _) => { output.push(item.id, ", ") }) - if (this.size > 0) output = output.slice(0, -1) - output.push("]") - return output.join("") - } -} diff --git a/test/suite/helpers.ts b/test/suite/helpers.ts index febc3ba..0dd09b1 100644 --- a/test/suite/helpers.ts +++ b/test/suite/helpers.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import { expect } from 'chai' -import { capture, instance, mock, when } from 'ts-mockito'; -import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from 'ts-mockito/lib/capture/ArgCaptor'; +import { capture, instance, mock, when } from '@typestrong/ts-mockito'; +import { ArgCaptor1, ArgCaptor2, ArgCaptor3 } from '@typestrong/ts-mockito/lib/capture/ArgCaptor'; import { TestSuiteManager } from '../testSuiteManager'; import { getExtensionLogger, IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 0d89e55..159924a 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import * as path from 'path' -import { anything, instance, verify, mock, when } from 'ts-mockito' +import { anything, instance, mock, reset, verify, when } from '@typestrong/ts-mockito' import { expect } from 'chai'; -import { before, beforeEach } from 'mocha'; +import { after, before, beforeEach } from 'mocha'; import { TestLoader } from '../../../src/testLoader'; import { TestSuiteManager } from '../../../src/testSuiteManager'; @@ -19,7 +19,6 @@ import { testStateCaptors, verifyFailure } from '../helpers'; -import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for Minitest', function() { let testController: vscode.TestController @@ -29,8 +28,10 @@ suite('Extension Test for Minitest', function() { let testLoader: TestLoader; let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; + let mockTestRun: vscode.TestRun; + let cancellationTokenSource: vscode.CancellationTokenSource; - const log = logger("info"); + const log = logger("off"); let expectedPath = (file: string): string => { return path.resolve( @@ -80,14 +81,33 @@ suite('Extension Test for Minitest', function() { when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) + + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + mockTestRun = mock() + cancellationTokenSource = new vscode.CancellationTokenSource() + testController.createTestRun = (_: vscode.TestRunRequest, name?: string): vscode.TestRun => { + when(mockTestRun.name).thenReturn(name) + when(mockTestRun.token).thenReturn(cancellationTokenSource.token) + return instance(mockTestRun) + } + + manager = new TestSuiteManager(log, testController, config) + testRunner = new TestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); + }) + + beforeEach(function() { + reset(mockTestRun); + }); + + after(function() { + testController.dispose() + cancellationTokenSource.dispose() }) suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); + testController.items.replace([]) }) test('Load tests on file resolve request', async function () { @@ -211,24 +231,15 @@ suite('Extension Test for Minitest', function() { }) suite('status events', function() { - let cancellationTokenSource = new vscode.CancellationTokenSource(); - before(async function() { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader['loadTests']() }) suite(`running collections emits correct statuses`, async function() { - let mockTestRun: vscode.TestRun - test('when running full suite', async function() { let mockRequest = setupMockRequest(manager) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) @@ -242,7 +253,6 @@ suite('Extension Test for Minitest', function() { let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) @@ -256,7 +266,6 @@ suite('Extension Test for Minitest', function() { let mockRequest = setupMockRequest(manager, ["abs_test.rb", "square/square_test.rb"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! // One less 'started' than the other tests as it doesn't include the 'square' folder verify(mockTestRun.enqueued(anything())).times(7) @@ -304,7 +313,7 @@ suite('Extension Test for Minitest', function() { let mockRequest = setupMockRequest(manager, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! + switch(status) { case "passed": testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 36f1cf8..0370fd8 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path' -import { anything, instance, verify, mock, when } from 'ts-mockito' -import { before, beforeEach } from 'mocha'; +import { anything, instance, mock, reset, verify, when } from '@typestrong/ts-mockito' +import { after, before, beforeEach } from 'mocha'; import { expect } from 'chai'; import { TestLoader } from '../../../src/testLoader'; @@ -19,7 +19,6 @@ import { TestItemExpectation, TestFailureExpectation } from '../helpers'; -import { StubTestController } from '../../stubs/stubTestController'; suite('Extension Test for RSpec', function() { let testController: vscode.TestController @@ -29,8 +28,10 @@ suite('Extension Test for RSpec', function() { let testLoader: TestLoader; let manager: TestSuiteManager; let resolveTestsProfile: vscode.TestRunProfile; + let mockTestRun: vscode.TestRun; + let cancellationTokenSource: vscode.CancellationTokenSource; - const log = logger("debug"); + const log = logger("off"); let expectedPath = (file: string): string => { return path.resolve( @@ -93,14 +94,33 @@ suite('Extension Test for RSpec', function() { when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) when(mockProfile.label).thenReturn('ResolveTests') resolveTestsProfile = instance(mockProfile) + + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + mockTestRun = mock() + cancellationTokenSource = new vscode.CancellationTokenSource() + testController.createTestRun = (_: vscode.TestRunRequest, name?: string): vscode.TestRun => { + when(mockTestRun.name).thenReturn(name) + when(mockTestRun.token).thenReturn(cancellationTokenSource.token) + return instance(mockTestRun) + } + + manager = new TestSuiteManager(log, testController, config) + testRunner = new TestRunner(log, manager, workspaceFolder) + testLoader = new TestLoader(log, resolveTestsProfile, manager); + }) + + beforeEach(function() { + reset(mockTestRun) + }); + + after(function() { + testController.dispose() + cancellationTokenSource.dispose() }) suite('dry run', function() { beforeEach(function () { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); + testController.items.replace([]) }) test('Load tests on file resolve request', async function () { @@ -197,7 +217,6 @@ suite('Extension Test for RSpec', function() { }) test('Load all tests', async function () { - console.log("resolving files in test") await testLoader['loadTests']() const manager = testController.items @@ -333,24 +352,15 @@ suite('Extension Test for RSpec', function() { }) suite('status events', function() { - let cancellationTokenSource = new vscode.CancellationTokenSource(); - before(async function() { - testController = new StubTestController(log) - manager = new TestSuiteManager(log, testController, config) - testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); await testLoader['loadTests']() }) suite(`running collections emits correct statuses`, async function() { - let mockTestRun: vscode.TestRun - test('when running full suite', async function() { let mockRequest = setupMockRequest(manager) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! verify(mockTestRun.enqueued(anything())).times(20) verify(mockTestRun.started(anything())).times(7) @@ -364,7 +374,6 @@ suite('Extension Test for RSpec', function() { let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) @@ -378,7 +387,6 @@ suite('Extension Test for RSpec', function() { let mockRequest = setupMockRequest(manager, ["abs_spec.rb", "square/square_spec.rb"]) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - mockTestRun = (testController as StubTestController).getMockTestRun(request)! verify(mockTestRun.enqueued(anything())).times(7) // One less 'started' than the other tests as it doesn't include the 'square' folder @@ -426,7 +434,7 @@ suite('Extension Test for RSpec', function() { let mockRequest = setupMockRequest(manager, expectedTest.id) let request = instance(mockRequest) await testRunner.runHandler(request, cancellationTokenSource.token) - let mockTestRun = (testController as StubTestController).getMockTestRun(request)! + switch(status) { case "passed": testItemMatches(testStateCaptors(mockTestRun).passedArg(0).testItem, expectedTest) diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index 0ab5443..f1a7496 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { spy, when } from 'ts-mockito' +import { spy, when } from '@typestrong/ts-mockito' import * as vscode from 'vscode' import * as path from 'path' diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 47c82aa..8d84cda 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -1,5 +1,5 @@ import { before, beforeEach, afterEach } from 'mocha'; -import { instance, mock, when } from 'ts-mockito' +import { instance, mock, when } from '@typestrong/ts-mockito' import * as childProcess from 'child_process'; import * as vscode from 'vscode' import * as path from 'path' @@ -42,7 +42,7 @@ suite('FrameworkProcess', function () { }) beforeEach(function () { - testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) }) @@ -232,7 +232,7 @@ suite('FrameworkProcess', function () { }) beforeEach(function () { - testController = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, testController, instance(config)) frameworkProcess = new FrameworkProcess(log, "testCommand", spawnOptions, cancellationTokenSource.token, manager) }) diff --git a/test/suite/unitTests/testSuiteManager.test.ts b/test/suite/unitTests/testSuiteManager.test.ts index a03809f..ac93dac 100644 --- a/test/suite/unitTests/testSuiteManager.test.ts +++ b/test/suite/unitTests/testSuiteManager.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { before, beforeEach, afterEach } from 'mocha'; -import { instance, mock, when } from 'ts-mockito' +import { instance, mock, when } from '@typestrong/ts-mockito' import * as vscode from 'vscode' import path from 'path' @@ -23,7 +23,7 @@ suite('TestSuiteManager', function () { }); beforeEach(function () { - controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); + controller = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer'); manager = new TestSuiteManager(log, controller, instance(mockConfig)) }); From 94577b580a42de775a99d829e8b0ff8c842cdaca Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 12:22:29 +0000 Subject: [PATCH 080/108] Forgot to rename the test status listener file after renaming the class --- src/testRunner.ts | 2 +- src/{testRunContext.ts => testStatusListener.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{testRunContext.ts => testStatusListener.ts} (100%) diff --git a/src/testRunner.ts b/src/testRunner.ts index 1abb840..32b0409 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import { IChildLogger } from '@vscode-logging/logger'; import { __asyncDelegator } from 'tslib'; -import { TestStatusListener } from './testRunContext'; +import { TestStatusListener } from './testStatusListener'; import { TestSuiteManager } from './testSuiteManager'; import { FrameworkProcess } from './frameworkProcess'; diff --git a/src/testRunContext.ts b/src/testStatusListener.ts similarity index 100% rename from src/testRunContext.ts rename to src/testStatusListener.ts From 2ba29152eb3ff90471a023025fafc7ab5b9a4e75 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 12:27:34 +0000 Subject: [PATCH 081/108] delete TODO file --- TODO | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index c950c1e..0000000 --- a/TODO +++ /dev/null @@ -1,3 +0,0 @@ -* Move logic for setting test info from test runner output from TestLoader into TestRunner and use it before we call handleStatus on line 371 -* Fix up minitest specs like I did for the RSpec ones -* Get rid of initTests and do it using TestRunner.spawnCancellable child like the actual test runs do From 84006f1c70a11d11cc13928ed0198b57915f95d6 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 22:31:40 +0000 Subject: [PATCH 082/108] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index adea8a2..b5d1b57 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Property | Description ---------------------------------------|--------------------------------------------------------------- `rubyTestExplorer.logpanel` | Whether to write diagnotic logs to an output panel. `rubyTestExplorer.logfile` | Write diagnostic logs to the given file. +`rubyTestExplorer.logLevel` | The level of information to log. One of `off` (no logs), `error`, `warn`, `info` (default), `debug`, or `trace` (all logs). Note: `debug` and `trace` are very noisy and are not reccomended for daily use. `rubyTestExplorer.testFramework` | `none`, `auto`, `rspec`, or `minitest`. `auto` by default, which automatically detects the test framework based on the gems listed by Bundler. Can disable the extension functionality with `none` or set the test framework explicitly, if auto-detect isn't working properly. `rubyTestExplorer.filePattern` | Define the pattern to match test files by, for example `["*_test.rb", "test_*.rb", "*_spec.rb"]`. `rubyTestExplorer.debuggerHost` | Define the host to connect the debugger to, for example `127.0.0.1`. @@ -92,7 +93,7 @@ There are two groups of tests included in the repository. - Tests for Ruby scripts to collect test information and run tests. Run with `bundle exec rake` in `ruby` directory. - Tests for VS Code extension which invokes the Ruby scripts. Run from VS Code's debug panel with the "Run tests for" configurations. - - There are separate debug configurations for each supported test framework. + - There are separate debug configurations for each supported test framework, as well as unit tests for the extension itself. - Note that you'll need to run `npm run build && npm run package` before you'll be able to successfully run the extension tests. You'll also need to re-run these every time you make changes to the extension code or your tests. You can see `.github/workflows/test.yml` for CI configurations. From f98b2a4eeb70d0d44d75bd4276375aeb4acb42fe Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 22:31:52 +0000 Subject: [PATCH 083/108] Add architecture doc --- docs/architecture.md | 136 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..316ee81 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,136 @@ +# Architecture + +This extension is essentially comprised of 4 main parts: + +- [Configuration and setup](#configuration-and-setup) +- [Management of the test tree state](#management-of-the-test-tree-state) +- [Discovering tests](#discovering-tests) + - [1. File changes](#1-file-changes) + - [2. Resolve handler](#2-resolve-handler) + - [Loading queue](#loading-queue) +- [Running tests](#running-tests) + +As much as possible, anything that might need to be cleaned up or cancelled extends the [Disposable](https://code.visualstudio.com/api/references/vscode-api#Disposable) interface, so that it provides a clear, uniform way for other classes to do so. + +## Configuration and setup + +These parts of the extension are the most straightforward: + +- `Config` + - Abstract base class for test framework configuration. + - Ensures that the rest of the extension does not need to care which test framework is being used when building commands to run, or getting file patterns, etc. +- `RspecConfig` & `MinitestConfig` + - Framework-specific subclasses of `Config`. + - Implement the abstract functions from `Config` as well as any other configuration/processing needed to supply the extension with the relevant data to interact correctly with their respective frameworks. +- `TestFactory` + - Creates the `TestLoader` and `TestRunner` instances used by the extension. + - Disposes of the `TestLoader` and `TestRunner` if necessary when the configuration changes, so that they clean up and terminate any running processes and can be recreated for the new configuration. +- `main.ts` + - Extension entry point - called by VSCode to intialize the extension + - Creates the logger, `TestController` (see [Management of the test tree state](#management-of-the-test-tree-state)), `TestFactory`, and `Config` instances + - Creates the three [TestRunProfile](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile) instances used by the extension (see [Running tests](#running-tests)) + - Creates the debug configuration used by the `Debug` profile + - Registers the controller, factory and profiles with VSCode to be disposed of when the extension is unloaded + - Registers a `resolveHandler` with the controller and initializes the `TestLoader` (see [Discovering tests](#discovering-tests)) + +## Management of the test tree state + +There are only two classes that deal with this: + +- [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController) + - Part of the VSCode API, and is the link between VSCode and this extension +- `TestSuiteManager` + - Provides functions for the rest of the extension to use to update and access the test tree. + +The [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController) instance is the heart of the VSCode testing API: + +- It is used to create [TestRunProfiles](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile), which make it easy for tests to be run in different ways +- It is used to create [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) which represent the various folders, files, describes/contexts/groups, and tests +- It holds the list of known tests allowing them to be displayed in the UI, and run/discovered via the handler functions registered on the controller or the profiles created with it. +- It is used to create [TestRuns](https://code.visualstudio.com/api/references/vscode-api#TestRun) from [TestRunRequests](https://code.visualstudio.com/api/references/vscode-api#TestRunRequest), which are used for reporting the statuses of tests that are run, as well as grouping results. + +The classes and functions provided by the VSCode API for managing the state of the test tree are, by necessity, very basic as they cannot predict what will be appropriate for any particular test provider. For example, the [TestItemCollection](https://code.visualstudio.com/api/references/vscode-api#TestItemCollection) used by the controller to hold the known tests cannot retrieve [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) that are children of the top-level items. + +Because of this, as well as other constraints we must satisfy when using the testing API, and to make things easier in the rest of the extension, we have the `TestSuiteManager` class which keeps all the logic for managing a hierarchy of tests in a single place. It takes care of the following: + +- Creating [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) instances to ensure that all the following constraints are always satisfied: + - Test IDs must all be unique. We use the relative path to the test from the test folder root, and its location, e.g. for the second RSpec test in `./spec/foo/bar_spec.rb`, the ID would be `foo/bar_spec.rb[1:2]`. + - Test item URIs are optional, but we want them to always be set with both the absolute file path and [Range](https://code.visualstudio.com/api/references/vscode-api#TestItem). + - `canResolveChildren` should be `true` for all items that can have children which is non-trivial to determine. + - Folders, files and some test groups don't get included in test framework output, so we need to ensure that all parent items are also created when an item needs creating. +- Retrieving [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) + - For the same reason that we need to create parents when creating items, we also have to walk the test tree to find a test item when needed. +- Deleting [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) + - To delete an item we also have to walk the tree to find the collection that contains it. +- Normalising IDs + - Because we use the relative path to a test file as part of the ID, it's helpful to allow other classes to not have to worry about things like stripping leading `/`s, `./`s, etc + +## Discovering tests + +([VS Code API docs](https://code.visualstudio.com/api/extension-guides/testing#discovering-tests)) + +Discovering tests is done by the `TestLoader` class in two main ways: + +### 1. File changes + +When the `TestLoader` is created, or when the configuration that affects finding test files is changed, a set of [FileSystemWatchers](https://code.visualstudio.com/api/references/vscode-api#FileSystemWatcher) are created using the configured file patterns. + +1. When a file is created: + A [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) is created for the new file and added to the tree. +2. When a file is changed: + The [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) for the changed file is retrieved from the tree, and enqueued to be loaded by the test framework to get new/updated information about the tests within it. +3. When a file is deleted: + The [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) for the deleted file is removed from the [TestItemCollection](https://code.visualstudio.com/api/references/vscode-api#TestItemCollection) that contains it, along with all its children. + +### 2. Resolve handler + +When the extension is initialized, a `resolveHandler` function is registered with the [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController). + +This function is called called whenever an item that may contain children is expanded in the test sidebar by clicking on the arrow icon, and is passed the relevant [TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) from the tree as a parameter. This item is then enqueued to be loaded by the test framework. + +This function It may also be called with no arguments to resolve all tests if a request to reload the entire tree is made, in which case the test framework is run immediately to load all tests. + +### Loading queue + +As mentioned, the `TestLoader` makes use of a queue for loading tests. The main reason for this is that the [FileSystemWatchers](https://code.visualstudio.com/api/references/vscode-api#FileSystemWatcher) only report changes one file at a time, and on large repositories this can easily result in hundreds of test processes being spawned in a short amount of time which will easily grind even powerful computers to a halt. + +To avoid this, tests are added to a queue to be loaded, which behaves as follows: + +- An async worker function checks the queue for [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) to run + - If any are found, it: + - Drains the queue, so that [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) enqueued while it is running don't get missed + - Sets the `busy` flag on all the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) - this causes a spinner to be displayed in the UI for those items. + - Creates a [TestRunRequest](https://code.visualstudio.com/api/references/vscode-api#TestItem) containing the items to be loaded, using the `ResolveTests` profile and runs it with the profile's `runHandler` (see below) to load all the tests that were in the queue. + - Once this is completed, it unsets the `busy` flag on the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) and checks the queue for more items. + - If the queue is empty, it creates a promise and waits for it to be resolved. Once resolved, it checks the queue again. +- When a test is added to the queue, if the async worker is waiting for items, the resolve function for the promise it is waiting on is resolved to notify it that items have been added. + +This ensures that only one test process at a time is running to load tests. + +## Running tests + +([VS Code API docs](https://code.visualstudio.com/api/extension-guides/testing#running-tests)) + +When the extension is initialized (see [Configuration and setup](#configuration-and-setup)), three [TestRunProfiles](https://code.visualstudio.com/api/references/vscode-api#TestRunProfile) are registered with the controller: + +- The `Run` profile, used for running tests (default profile for the `Run` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) +- The `Debug` profile, used for debugging tests (default profile for the `Debug` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) +- The `ResolveTests` profile, used for loading tests (using the `Run` [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind)) + - This profile can only be used internally by the extension, and is used by the `TestLoader` for loading tests + +Note: There is a third possible profile kind, `Profile`, intended to be used for profiling tests that is not currently used by this extension. + +When a user runs/debugs one or more tests from the UI, the `runHandler` function associated with the default profile for that [profile kind](https://code.visualstudio.com/api/references/vscode-api#TestRunProfileKind) is called. For all three profiles, this is `TestRunner.runHandler`. + +The `runHandler` does the following: + +- Creates a [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) from the [TestRunRequest](https://code.visualstudio.com/api/references/vscode-api#TestRunRequest) passed in as a parameter +- If the profile in the request is a `Debug` profile, it starts a debugging session and continues +- Marks all the [TestItems](https://code.visualstudio.com/api/references/vscode-api#TestItem) in the request as enqueued. +- Builds the command to run the requested tests (obtained from the `RspecConfig`/`MinitestConfig` classes, as appropriate) + - If the profile in the request is the `ResolveTests` profile, it builds a dry-run (RSpec)/list tests (Minitest) command +- Creates a `FrameworkProcess` instance, passing in the command to be run + - `FrameworkProcess` is a wrapper around the `child_process` in which the test framework runs. It parses the output, and emits status events based on it, and handles the lifetime of the child process, terminating it early if needed. +- Registers a `TestStatusListener` with the `FrameworkProcess` to call the [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) with information about the test results as they are received. +- Tells the `FrameworkProcess` instance to spawn the child process and waits for it to finish +- Calls `end` on the [TestRun](https://code.visualstudio.com/api/references/vscode-api#TestRun) to let VSCode know the test run is finished. From d098e884f3d88c7883b6450fd6e64ae31d7507fc Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 22:52:45 +0000 Subject: [PATCH 084/108] Add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3446da7..147bb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Changed +- Rewrite extension to use the native [VSCode testing API](https://code.visualstudio.com/api/extension-guides/testing) instead of the older one provided by the [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) extension. + - Allows for partial test tree updates, lazy loading of tests, much more responsive UI, filtering of tests, as well as greater efficiency by interacting directly with VSCode instead of going through another extension, and much more. ## [0.9.1] - 2022-08-04 ### Fixed From 0480f7986ff73762e8c711309bb0fbd08f1d0abe Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Sun, 15 Jan 2023 22:53:10 +0000 Subject: [PATCH 085/108] Add ability to run all test suites in one go, for convenince --- test/runFrameworkTests.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/runFrameworkTests.ts b/test/runFrameworkTests.ts index 0df798d..a97437e 100644 --- a/test/runFrameworkTests.ts +++ b/test/runFrameworkTests.ts @@ -2,7 +2,7 @@ import * as path from 'path' import { runTests, downloadAndUnzipVSCode } from '@vscode/test-electron'; const extensionDevelopmentPath = path.resolve(__dirname, '../../'); -const allowedSuiteArguments = ["rspec", "minitest", "unitTests"] +const allowedSuiteArguments = ["all", "rspec", "minitest", "unitTests"] const maxDownloadRetries = 5; async function main(framework: string) { @@ -16,7 +16,13 @@ async function main(framework: string) { } } if (vscodeExecutablePath) { - await runTestSuite(vscodeExecutablePath, framework) + if (framework == 'all') { + await runTestSuite(vscodeExecutablePath, 'unitTests') + await runTestSuite(vscodeExecutablePath, 'rspec') + await runTestSuite(vscodeExecutablePath, 'minitest') + } else { + await runTestSuite(vscodeExecutablePath, framework) + } } else { console.error("crap") } From 9ce2492403f7137420470dfd67fc119187518f62 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 00:46:49 +0000 Subject: [PATCH 086/108] Revert unnecessary change to ruby/Rakefile used to test changes in custom_formatter.rb --- ruby/Rakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/ruby/Rakefile b/ruby/Rakefile index 3291537..d76c4fb 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -3,7 +3,6 @@ require "rake/testtask" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:rspectest) do |t| - t.rspec_opts = ['--require /home/tabby/git/vscode-ruby-test-adapter/ruby/custom_formatter.rb', '--format CustomFormatter'] t.pattern = ['rspecs/**/*_spec.rb', 'rspecs/**/*_test.rb'] end From c2aa46db5fd2248f5fd2c6605e0ba14c5a40bf8d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 07:39:06 +0000 Subject: [PATCH 087/108] Revert unnecessary change to ruby/rspecs, also used to test changes in custom_formatter.rb --- ruby/rspecs/unit_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/rspecs/unit_test.rb b/ruby/rspecs/unit_test.rb index 1a50db6..dc9ee79 100644 --- a/ruby/rspecs/unit_test.rb +++ b/ruby/rspecs/unit_test.rb @@ -16,6 +16,6 @@ def square_of(n) end it "finds the square of 3" do - expect(@calculator.square_of(3)).to eq + expect(@calculator.square_of(3)).to eq(9) end end \ No newline at end of file From 08d46f341955640bb85baeda085cf68589a243d1 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 07:57:51 +0000 Subject: [PATCH 088/108] Change rake_task_test assertions to not be dependent on order Probably due to the recent inclusion of a seed parameter for minitest, the assertions in this test sometimes failed due to the test output being in a different order to the expectations. Now the order doesn't matter so it should always pass as long as all the expected results did happen. --- ruby/test/minitest/rake_task_test.rb | 97 ++++++++++++++-------------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/ruby/test/minitest/rake_task_test.rb b/ruby/test/minitest/rake_task_test.rb index 9ce697d..2835a24 100644 --- a/ruby/test/minitest/rake_task_test.rb +++ b/ruby/test/minitest/rake_task_test.rb @@ -82,55 +82,54 @@ def test_test_list stdout =~ /START_OF_TEST_JSON(.*)END_OF_TEST_JSON/ json = JSON.parse($1, symbolize_names: true) - assert_equal( - [ - { - description: "square of one", - full_description: "square of one", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 4, - klass: "SquareTest", - method: "test_square_of_one", - runnable: "SquareTest", - id: "./test/square_test.rb[4]" - }, - { - description: "square of two", - full_description: "square of two", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 8, - klass: "SquareTest", - method: "test_square_of_two", - runnable: "SquareTest", - id: "./test/square_test.rb[8]" - }, - { - description: "square error", - full_description: "square error", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 12, - klass: "SquareTest", - method: "test_square_error", - runnable: "SquareTest", - id: "./test/square_test.rb[12]" - }, - { - description: "square skip", - full_description: "square skip", - file_path: "./test/square_test.rb", - full_path: (dir + "test/square_test.rb").to_s, - line_number: 16, - klass: "SquareTest", - method: "test_square_skip", - runnable: "SquareTest", - id: "./test/square_test.rb[16]" - } - ], - json[:examples] - ) + [ + { + description: "square of one", + full_description: "square of one", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 4, + klass: "SquareTest", + method: "test_square_of_one", + runnable: "SquareTest", + id: "./test/square_test.rb[4]" + }, + { + description: "square of two", + full_description: "square of two", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 8, + klass: "SquareTest", + method: "test_square_of_two", + runnable: "SquareTest", + id: "./test/square_test.rb[8]" + }, + { + description: "square error", + full_description: "square error", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 12, + klass: "SquareTest", + method: "test_square_error", + runnable: "SquareTest", + id: "./test/square_test.rb[12]" + }, + { + description: "square skip", + full_description: "square skip", + file_path: "./test/square_test.rb", + full_path: (dir + "test/square_test.rb").to_s, + line_number: 16, + klass: "SquareTest", + method: "test_square_skip", + runnable: "SquareTest", + id: "./test/square_test.rb[16]" + } + ].each do |expectation| + assert_includes(json[:examples], expectation) + end end def test_test_run_all From 25b31db7091044970a2b47f8f3e59b92fcd9a19e Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 10:19:45 +0000 Subject: [PATCH 089/108] Log test statuses and unrecognised stdout messages at info level --- src/frameworkProcess.ts | 8 ++++---- src/testStatusListener.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index b979812..ef6376e 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -116,12 +116,12 @@ export class FrameworkProcess implements vscode.Disposable { } try { if (data.includes('START_OF_TEST_JSON')) { - log.trace("Received test run results", data); + log.trace("Received test run results: %s", data); this.parseAndHandleTestOutput(data); } else { const match = this.statusPattern.exec(data) if (match && match.groups) { - log.trace("Received test status event", data); + log.trace("Received test status event: %s", data); const id = match.groups['id'] const status = match.groups['status'] const exception = match.groups['exceptionClass'] @@ -145,11 +145,11 @@ export class FrameworkProcess implements vscode.Disposable { testMessage )) } else { - log.trace("Ignoring unrecognised output", data) + log.info("stdout: %s", data) } } } catch (err) { - log.error('Error parsing output', err) + log.error('Error parsing output', { error: err }) } } diff --git a/src/testStatusListener.ts b/src/testStatusListener.ts index caeb35d..936e168 100644 --- a/src/testStatusListener.ts +++ b/src/testStatusListener.ts @@ -17,30 +17,30 @@ export class TestStatusListener { switch(event.status) { case Status.skipped: - log.debug('Received test skipped event: %s', event.testItem.id) + log.info('Test skipped: %s', event.testItem.id) testRun.skipped(event.testItem) break; case Status.passed: if (this.isTestLoad(profile)) { - log.debug('Ignored test passed event from test load: %s (duration: %d)', event.testItem.id, event.duration) + log.info('Test loaded: %s (duration: %d)', event.testItem.id, event.duration) } else { - log.debug('Received test passed event: %s (duration: %d)', event.testItem.id, event.duration) + log.info('Test passed: %s (duration: %d)', event.testItem.id, event.duration) testRun.passed(event.testItem, event.duration) } break; case Status.errored: - log.debug('Received test errored event: %s (duration: %d)', event.testItem.id, event.duration, event.message) + log.info('Test errored: %s (duration: %d)', event.testItem.id, event.duration, event.message) testRun.errored(event.testItem, event.message!, event.duration) break; case Status.failed: - log.debug('Received test failed event: %s (duration: %d)', event.testItem.id, event.duration, event.message) + log.info('Test failed: %s (duration: %d)', event.testItem.id, event.duration, event.message) testRun.failed(event.testItem, event.message!, event.duration) break; case Status.running: if (this.isTestLoad(profile)) { log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) } else { - log.debug('Received test started event: %s', event.testItem.id) + log.info('Test started: %s', event.testItem.id) testRun.started(event.testItem) } break; From fcc21854069ccefac5ae437aeb2248da0ebec41b Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 11:27:31 +0000 Subject: [PATCH 090/108] Improve logging of child process output for when things don't work --- src/frameworkProcess.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index ef6376e..ceaaa9f 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -67,7 +67,7 @@ export class FrameworkProcess implements vscode.Disposable { } try { - this.log.debug('Starting child process') + this.log.debug('Starting child process', { env: this.spawnArgs }) this.childProcess = childProcess.spawn( this.testCommand, this.spawnArgs @@ -76,9 +76,11 @@ export class FrameworkProcess implements vscode.Disposable { this.childProcess.stderr!.pipe(split2()).on('data', (data) => { let log = this.log.getChildLogger({label: 'stderr'}) data = data.toString(); - log.trace(data); if (data.startsWith('Fast Debugger') && onDebugStarted) { + log.info('Notifying debug session that test process is ready to debug'); onDebugStarted() + } else { + log.warn('%s', data); } }) @@ -94,7 +96,7 @@ export class FrameworkProcess implements vscode.Disposable { this.log.trace('Child process exited', code, signal) }); this.childProcess!.once('close', (code: number, signal: string) => { - this.log.debug('Child process exited, and all streams closed', code, signal) + this.log.debug('Child process exited, and all streams closed', { exitCode: code, signal: signal }) resolve({code, signal}); }); this.childProcess!.once('error', (err: Error) => { From 524a04062622ba495f1891c68b5d70f88a00b2b1 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 12:14:49 +0000 Subject: [PATCH 091/108] Ensure exit codes other than 0 are actually treated as errors --- src/frameworkProcess.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index ceaaa9f..06f87c5 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -93,14 +93,19 @@ export class FrameworkProcess implements vscode.Disposable { return await new Promise<{code:number, signal:string}>((resolve, reject) => { this.childProcess!.once('exit', (code: number, signal: string) => { - this.log.trace('Child process exited', code, signal) + this.log.trace('Child process exited', { exitCode: code, signal: signal }) }); this.childProcess!.once('close', (code: number, signal: string) => { - this.log.debug('Child process exited, and all streams closed', { exitCode: code, signal: signal }) - resolve({code, signal}); + if (code == 0) { + this.log.debug('Child process exited successfully, and all streams closed', { exitCode: code, signal: signal }) + resolve({code, signal}); + } else { + this.log.error('Child process exited abnormally, and all streams closed', { exitCode: code, signal: signal }) + reject(new Error(`Child process exited abnormally. Status code: ${code}${signal ? `, signal: ${signal}` : ''}`)); + } }); this.childProcess!.once('error', (err: Error) => { - this.log.debug('Error event from child process', err.message) + this.log.error('Error event from child process: %s', err.message) reject(err); }); }) From 2764bcf777f31ccc68479efa2a95b109e412addd Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 15:58:10 +0000 Subject: [PATCH 092/108] Add extra debug logging around disposing/cancellation --- src/frameworkProcess.ts | 11 ++++++++++- src/loaderQueue.ts | 2 +- src/testFactory.ts | 15 +++++++++------ src/testLoader.ts | 1 + src/testRunner.ts | 5 ++++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 06f87c5..4b76660 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -48,8 +48,17 @@ export class FrameworkProcess implements vscode.Disposable { } dispose() { + this.log.debug("Dispose called") this.isDisposed = true - this.childProcess?.kill() + if (this.childProcess) { + if (this.childProcess.kill()) { + this.log.debug("Child process killed") + } else { + this.log.debug("Attempt to kill child process failed") + } + } else { + this.log.debug("Child process not running - not killing") + } for (const disposable of this.disposables) { try { disposable.dispose() diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts index 458607c..37bd7ee 100644 --- a/src/loaderQueue.ts +++ b/src/loaderQueue.ts @@ -44,9 +44,9 @@ export class LoaderQueue implements vscode.Disposable { * from the queue and that it must terminate, then waits for the worker function to finish */ dispose() { - // TODO: Terminate child process this.log.info('disposed') this.isDisposed = true + this.queue.clear() if (this.terminateQueueWorker) { // Stop the worker function from waiting for more items this.log.debug('notifying worker for disposal') diff --git a/src/testFactory.ts b/src/testFactory.ts index f9913f9..fb1759d 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { IVSCodeExtLogger } from "@vscode-logging/logger"; +import { IChildLogger, IVSCodeExtLogger } from "@vscode-logging/logger"; import { Config } from './config'; import { TestLoader } from './testLoader'; import { RspecConfig } from './rspec/rspecConfig'; @@ -13,6 +13,7 @@ import { TestRunner } from './testRunner'; * Also takes care of disposing them when required */ export class TestFactory implements vscode.Disposable { + private readonly log: IChildLogger; private isDisposed = false; private loader: TestLoader | null = null; private runner: TestRunner | null = null; @@ -21,18 +22,20 @@ export class TestFactory implements vscode.Disposable { private manager: TestSuiteManager constructor( - private readonly log: IVSCodeExtLogger, + private readonly rootLog: IVSCodeExtLogger, private readonly controller: vscode.TestController, private config: Config, private readonly profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile }, private readonly workspace?: vscode.WorkspaceFolder, ) { + this.log = rootLog.getChildLogger({ label: `${TestFactory.name}` }) this.disposables.push(this.configWatcher()); - this.framework = Config.getTestFramework(this.log); + this.framework = Config.getTestFramework(this.rootLog); this.manager = new TestSuiteManager(this.log, this.controller, this.config) } dispose(): void { + this.log.debug("Dispose called") this.isDisposed = true for (const disposable of this.disposables) { try { @@ -58,7 +61,7 @@ export class TestFactory implements vscode.Disposable { this.checkIfDisposed() if (!this.runner) { this.runner = new TestRunner( - this.log, + this.rootLog, this.manager, this.workspace, ) @@ -79,7 +82,7 @@ export class TestFactory implements vscode.Disposable { this.checkIfDisposed() if (!this.loader) { this.loader = new TestLoader( - this.log, + this.rootLog, this.profiles.resolveTestsProfile, this.manager ) @@ -110,7 +113,7 @@ export class TestFactory implements vscode.Disposable { return vscode.workspace.onDidChangeConfiguration(configChange => { this.log.info('Configuration changed'); if (configChange.affectsConfiguration("rubyTestExplorer.testFramework")) { - let newFramework = Config.getTestFramework(this.log); + let newFramework = Config.getTestFramework(this.rootLog); if (newFramework !== this.framework) { // Config has changed to a different framework - recreate test loader and runner this.config = newFramework == "rspec" diff --git a/src/testLoader.ts b/src/testLoader.ts index 8741a8d..f07bcdf 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -29,6 +29,7 @@ export class TestLoader implements vscode.Disposable { } dispose(): void { + this.log.debug("Dispose called") for (const disposable of this.disposables) { try { disposable.dispose(); diff --git a/src/testRunner.ts b/src/testRunner.ts index 32b0409..034f687 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -27,6 +27,7 @@ export class TestRunner implements vscode.Disposable { } public dispose() { + this.log.debug("Dispose called") for (const disposable of this.disposables) { try { disposable.dispose(); @@ -153,7 +154,9 @@ export class TestRunner implements vscode.Disposable { testRun.end(); } if (token.isCancellationRequested) { - log.info('Test run aborted due to cancellation') + log.info('Test run aborted due to cancellation - token passed into runHandler') + } else if (testRun.token.isCancellationRequested) { + log.info('Test run aborted due to cancellation - token from controller via TestRun') } } From 954965e7ad2fda28498f34f7bcb155e7b57b49f8 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 16 Jan 2023 17:14:12 +0000 Subject: [PATCH 093/108] Update expectations in rake_task_test --- ruby/test/minitest/rake_task_test.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ruby/test/minitest/rake_task_test.rb b/ruby/test/minitest/rake_task_test.rb index 2835a24..8e4788c 100644 --- a/ruby/test/minitest/rake_task_test.rb +++ b/ruby/test/minitest/rake_task_test.rb @@ -147,14 +147,14 @@ def test_test_run_all assert_any(examples, pass_count: 1) do |example| assert_equal "square error", example[:description] - assert_equal "failed", example[:status] + assert_equal "errored", example[:status] assert_nil example[:pending_message] refute_nil example[:exception] assert_equal "Minitest::UnexpectedError", example.dig(:exception, :class) assert_match(/RuntimeError:/, example.dig(:exception, :message)) assert_instance_of Array, example.dig(:exception, :backtrace) assert_instance_of Array, example.dig(:exception, :full_backtrace) - assert_equal 13, example.dig(:exception, :position) + assert_equal 12, example.dig(:exception, :position) end assert_any(examples, pass_count: 1) do |example| @@ -173,12 +173,12 @@ def test_test_run_all assert_equal "Expected: 3\n Actual: 4", example.dig(:exception, :message) assert_instance_of Array, example.dig(:exception, :backtrace) assert_instance_of Array, example.dig(:exception, :full_backtrace) - assert_equal 9, example.dig(:exception, :position) + assert_equal 8, example.dig(:exception, :position) end assert_any(examples, pass_count: 1) do |example| assert_equal "square skip", example[:description] - assert_equal "failed", example[:status] + assert_equal "skipped", example[:status] assert_equal "This is skip", example[:pending_message] assert_nil example[:exception] end @@ -218,7 +218,7 @@ def test_test_run_file_line assert_any(examples, pass_count: 1) do |example| assert_equal "square skip", example[:description] - assert_equal "failed", example[:status] + assert_equal "skipped", example[:status] end end end From 3f1a74c4e704623f1a76ff4dcc6a746f64144f79 Mon Sep 17 00:00:00 2001 From: Tabitha Cromarty Date: Tue, 17 Jan 2023 09:42:42 +0000 Subject: [PATCH 094/108] Apply @naveg's fix for ID segment splitting causing high CPU usage Co-authored-by: Evan Goldenberg --- src/testSuiteManager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/testSuiteManager.ts b/src/testSuiteManager.ts index bf5c6cb..4326e98 100644 --- a/src/testSuiteManager.ts +++ b/src/testSuiteManager.ts @@ -123,13 +123,15 @@ export class TestSuiteManager { testId = this.normaliseTestId(testId) // Split path segments - let idSegments = testId.split(path.sep) - log.debug('id segments', idSegments) - if (idSegments[0] === "") { - idSegments.splice(0, 1) + let originalSegments = testId.split(path.sep) + log.debug('originalSegments', originalSegments) + if (originalSegments[0] === "") { + originalSegments.splice(0, 1) } + + let idSegments = [...originalSegments] for (let i = 1; i < idSegments.length - 1; i++) { - let precedingSegments = idSegments.slice(0, i + 1) + let precedingSegments = originalSegments.slice(0, i + 1) idSegments[i] = path.join(...precedingSegments) } From 1058978a048a35b3fd413d1c4c9a0337949ed419 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 09:59:53 +0000 Subject: [PATCH 095/108] Attempt to fix repeated loading of children --- src/main.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main.ts b/src/main.ts index 9b7573d..b4c025e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -105,11 +105,8 @@ export async function activate(context: vscode.ExtensionContext) { log.debug('resolveHandler called', test) if (!test) { await factory.getLoader().discoverAllFilesInWorkspace(); - } else if (test.id.endsWith(".rb") || test.id.endsWith(']')) { - // Only parse files - if (!test.canResolveChildren) { - log.warn("resolveHandler called for test that can't resolve children: %s", test.id) - } + } 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); } }; From 0ca7e556264072b08168010be05ba7a756dffe45 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 16:49:02 +0000 Subject: [PATCH 096/108] Fix error in custom_formatter.rb if exception.backtrace_locations is nil --- ruby/custom_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/custom_formatter.rb b/ruby/custom_formatter.rb index 3cbf4f4..8dcd33c 100644 --- a/ruby/custom_formatter.rb +++ b/ruby/custom_formatter.rb @@ -128,7 +128,7 @@ def format_example(example) end def exception_position(backtrace, metadata) - location = backtrace.find { |frame| frame.path.end_with?(metadata[:file_path]) } + location = backtrace&.find { |frame| frame.path.end_with?(metadata[:file_path]) } return metadata[:line_number] unless location location.lineno From 012b60338a7a22100f90654e15bd0d2eb1ecfbd5 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 16:53:48 +0000 Subject: [PATCH 097/108] Don't report error/failure statuses until the end of the run We don't have the full exception data yet when the failure is first reported, and VSC only displays the first error/failure report it receives for a test so it's important to make sure the one we report has the correct backtrace & output so that it's displayed correctly --- src/frameworkProcess.ts | 18 +++--------------- src/testStatusListener.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 4b76660..e66222f 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -140,25 +140,13 @@ export class FrameworkProcess implements vscode.Disposable { log.trace("Received test status event: %s", data); const id = match.groups['id'] const status = match.groups['status'] - const exception = match.groups['exceptionClass'] - const message = match.groups['exceptionMessage'] let testItem = getTest(id) - let testMessage: vscode.TestMessage | undefined = undefined - if (message) { - testMessage = new vscode.TestMessage(exception ? `${exception}: ${message}` : message) - // TODO?: get actual exception location, not test location - if (testItem.uri && testItem.range) { - // it should always have a uri, but just to be safe... - testMessage.location = new vscode.Location(testItem.uri, testItem.range) - } else { - log.error('Test missing location details', { testId: testItem.id, uri: testItem.uri }) - } - } + this.testStatusEmitter.fire(new TestStatus( testItem, Status[status.toLocaleLowerCase() as keyof typeof Status], - undefined, // TODO?: get duration info here if possible - testMessage + // undefined, // TODO?: get duration info here if possible + // errorMessage, // TODO: get exception info here once we can send full exception data )) } else { log.info("stdout: %s", data) diff --git a/src/testStatusListener.ts b/src/testStatusListener.ts index 936e168..3591040 100644 --- a/src/testStatusListener.ts +++ b/src/testStatusListener.ts @@ -30,11 +30,15 @@ export class TestStatusListener { break; case Status.errored: log.info('Test errored: %s (duration: %d)', event.testItem.id, event.duration, event.message) - testRun.errored(event.testItem, event.message!, event.duration) + if (event.message) { + testRun.errored(event.testItem, event.message, event.duration) + } break; case Status.failed: log.info('Test failed: %s (duration: %d)', event.testItem.id, event.duration, event.message) - testRun.failed(event.testItem, event.message!, event.duration) + if (event.message) { + testRun.failed(event.testItem, event.message, event.duration) + } break; case Status.running: if (this.isTestLoad(profile)) { From 8bafc72cbecc9d87ea916088abd0b128ebf06709 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 17:03:31 +0000 Subject: [PATCH 098/108] Change log level when logLevel config setting is changed --- src/testFactory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/testFactory.ts b/src/testFactory.ts index fb1759d..f3825a1 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { IChildLogger, IVSCodeExtLogger } from "@vscode-logging/logger"; +import { IChildLogger, IVSCodeExtLogger, LogLevel } from "@vscode-logging/logger"; import { Config } from './config'; import { TestLoader } from './testLoader'; import { RspecConfig } from './rspec/rspecConfig'; @@ -129,6 +129,11 @@ export class TestFactory implements vscode.Disposable { } } } + if (configChange.affectsConfiguration("rubyTestExplorer.logLevel")) { + this.rootLog.changeLevel( + vscode.workspace.getConfiguration('rubyTestExplorer', null).get('logLevel') as LogLevel + ) + } }) } From 78b6f50e00e05ffe89a3790629425738fede3b54 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 18:07:34 +0000 Subject: [PATCH 099/108] Don't run whole file when only RSpec context should be run (also fix tests) --- src/testRunner.ts | 15 +++++++++------ test/suite/minitest/minitest.test.ts | 22 ++++++++++------------ test/suite/rspec/rspec.test.ts | 22 ++++++++++------------ 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index 034f687..753edcb 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -121,13 +121,16 @@ export class TestRunner implements vscode.Disposable { log.trace('Adding test to command: %s', node.id) // Mark selected tests as started this.enqueTestAndChildren(node, testRun) - command = `${command} ${node.uri?.fsPath}` - if (!node.canResolveChildren) { - // single test - if (!node.range) { - throw new Error(`Test item is missing line number: ${node.id}`) + if (node.id.includes('[')) { + if (this.manager.config.frameworkName() == 'RSpec') { + let locationStartIndex = node.id.lastIndexOf('[') + 1 + let locationEndIndex = node.id.lastIndexOf(']') + command = `${command} ${node.uri?.fsPath}[${node.id.slice(locationStartIndex, locationEndIndex)}]` + } else { + command = `${command} ${node.uri?.fsPath}:${node.range!.start.line + 1}` } - command = `${command}:${node.range!.start.line + 1}` + } else { + command = `${command} ${node.uri?.fsPath}` } log.trace("Current command: %s", command) } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index 159924a..db1f0cd 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -244,8 +244,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) @@ -257,8 +257,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) @@ -271,8 +271,8 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.enqueued(anything())).times(7) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) }) @@ -324,19 +324,17 @@ suite('Extension Test for Minitest', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "failed": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {...failureExpectation!, line: failureExpectation!.line! - 1}) - verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) verify(mockTestRun.errored(anything(), anything(), anything())).times(0) verify(mockTestRun.skipped(anything())).times(0) break; case "errored": - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, {...failureExpectation!, line: failureExpectation!.line! - 1}) - verifyFailure(1, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(0) break; case "skipped": diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index 0370fd8..ab91618 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -365,8 +365,8 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.enqueued(anything())).times(20) verify(mockTestRun.started(anything())).times(7) verify(mockTestRun.passed(anything(), anything())).times(8) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) @@ -378,8 +378,8 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.enqueued(anything())).times(8) verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) @@ -392,8 +392,8 @@ suite('Extension Test for RSpec', function() { // One less 'started' than the other tests as it doesn't include the 'square' folder verify(mockTestRun.started(anything())).times(5) verify(mockTestRun.passed(anything(), anything())).times(4) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(2) }) }) @@ -445,19 +445,17 @@ suite('Extension Test for RSpec', function() { verify(mockTestRun.skipped(anything())).times(0) break; case "failed": - verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, {...failureExpectation!, line: expectedTest.line}) - verifyFailure(1, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).failedArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) - verify(mockTestRun.failed(anything(), anything(), anything())).times(2) + verify(mockTestRun.failed(anything(), anything(), anything())).times(1) verify(mockTestRun.errored(anything(), anything(), anything())).times(0) verify(mockTestRun.skipped(anything())).times(0) break; case "errored": - verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, {...failureExpectation!, line: expectedTest.line}) - verifyFailure(1, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) + verifyFailure(0, testStateCaptors(mockTestRun).erroredArgs, expectedTest, failureExpectation!) verify(mockTestRun.passed(anything(), anything())).times(0) verify(mockTestRun.failed(anything(), anything(), anything())).times(0) - verify(mockTestRun.errored(anything(), anything(), anything())).times(2) + verify(mockTestRun.errored(anything(), anything(), anything())).times(1) verify(mockTestRun.skipped(anything())).times(0) break; case "skipped": From f235b5dd9e04b5747ec16ac931d28bb91cd2c1af Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 17 Jan 2023 14:05:34 +0000 Subject: [PATCH 100/108] Improve error logging if test process fails to start --- src/frameworkProcess.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index e66222f..7479a9e 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -29,6 +29,8 @@ export class FrameworkProcess implements vscode.Disposable { protected readonly log: IChildLogger; private readonly disposables: vscode.Disposable[] = [] private isDisposed = false; + private testRunStarted = false; + private preTestErrorLines: string[] = [] public readonly testStatusEmitter: vscode.EventEmitter = new vscode.EventEmitter() private readonly statusPattern = new RegExp(/(?RUNNING|PASSED|FAILED|ERRORED|SKIPPED)(:?\((:?(?(:?\w*(:?::)?)*)\:)?\s*(?.*)\))?\: (?.*)/) @@ -45,6 +47,19 @@ export class FrameworkProcess implements vscode.Disposable { this.log.debug('Cancellation requested') this.dispose() })) + + /* + * Create a listener so that we know when any tests have actually started running. Until this happens, any output + * is likely to be an error message and needs collecting up to be logged in one message. + */ + let testRunStartedListener = this.testStatusEmitter.event((e) => { + if (e.status == Status.running) { + this.log.info('Test run started - stopped capturing error output', { event: e }) + this.testRunStarted = true + testRunStartedListener.dispose() + } + }) + this.disposables.push(testRunStartedListener) } dispose() { @@ -89,7 +104,11 @@ export class FrameworkProcess implements vscode.Disposable { log.info('Notifying debug session that test process is ready to debug'); onDebugStarted() } else { - log.warn('%s', data); + if (this.testRunStarted) { + log.warn('%s', data); + } else { + this.preTestErrorLines.push(data) + } } }) @@ -119,6 +138,9 @@ export class FrameworkProcess implements vscode.Disposable { }); }) } finally { + if (this.preTestErrorLines.length > 0) { + this.log.error('Test process failed to run', { message: this.preTestErrorLines }) + } this.dispose() } } @@ -149,7 +171,11 @@ export class FrameworkProcess implements vscode.Disposable { // errorMessage, // TODO: get exception info here once we can send full exception data )) } else { - log.info("stdout: %s", data) + if (this.testRunStarted) { + log.info("stdout: %s", data) + } else { + this.preTestErrorLines.push(data) + } } } } catch (err) { From 96bbefe1621fc358feaf50bf188242deebc1faaf Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 18 Jan 2023 11:03:19 +0000 Subject: [PATCH 101/108] Add tests for FrameworkProcess output and exit code handling --- src/config.ts | 2 +- src/frameworkProcess.ts | 11 +- src/minitest/minitestConfig.ts | 4 +- src/rspec/rspecConfig.ts | 7 +- src/testRunner.ts | 32 ++- test/suite/unitTests/frameworkProcess.test.ts | 248 +++++++++++++++++- 6 files changed, 278 insertions(+), 26 deletions(-) diff --git a/src/config.ts b/src/config.ts index 031b9cc..dd30b73 100644 --- a/src/config.ts +++ b/src/config.ts @@ -85,7 +85,7 @@ export abstract class Config { * * @param testItems Array of TestItems to resolve children of, or undefined to resolve all tests */ - public abstract getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string + public abstract getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } /** * Get the env vars to run the subprocess with. diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 7479a9e..69f3ee2 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -54,8 +54,9 @@ export class FrameworkProcess implements vscode.Disposable { */ let testRunStartedListener = this.testStatusEmitter.event((e) => { if (e.status == Status.running) { - this.log.info('Test run started - stopped capturing error output', { event: e }) + this.log.info('Test run started - stopped capturing error output', { event: e }) this.testRunStarted = true + this.preTestErrorLines = [] testRunStartedListener.dispose() } }) @@ -84,6 +85,7 @@ export class FrameworkProcess implements vscode.Disposable { } public async startProcess( + args: string[], onDebugStarted?: (value: void | PromiseLike) => void, ) { if (this.isDisposed) { @@ -92,10 +94,7 @@ export class FrameworkProcess implements vscode.Disposable { try { this.log.debug('Starting child process', { env: this.spawnArgs }) - this.childProcess = childProcess.spawn( - this.testCommand, - this.spawnArgs - ) + this.childProcess = childProcess.spawn(this.testCommand, args, this.spawnArgs) this.childProcess.stderr!.pipe(split2()).on('data', (data) => { let log = this.log.getChildLogger({label: 'stderr'}) @@ -104,7 +103,7 @@ export class FrameworkProcess implements vscode.Disposable { log.info('Notifying debug session that test process is ready to debug'); onDebugStarted() } else { - if (this.testRunStarted) { + if (this.testRunStarted) { log.warn('%s', data); } else { this.preTestErrorLines.push(data) diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index c6603c6..9d9c3ac 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -70,8 +70,8 @@ export class MinitestConfig extends Config { return this.testCommandWithDebugger(debugConfiguration) }; - public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { - return `${this.getTestCommand()} vscode:minitest:list` + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } { + return { command: `${this.getTestCommand()} vscode:minitest:list`, args: [] } } /** diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index b581445..935ca33 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -84,14 +84,15 @@ export class RspecConfig extends Config { return this.testCommandWithFormatterAndDebugger(debugConfiguration) }; - public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } { let cmd = `${this.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; + let args: string[] = [] testItems?.forEach((item) => { let testPath = path.join(this.getAbsoluteTestDirectory(), item.id) - cmd = `${cmd} "${testPath}"` + args.push(testPath) }) - return cmd + return { command: cmd, args: args } } /** diff --git a/src/testRunner.ts b/src/testRunner.ts index 753edcb..aecd15d 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -100,23 +100,32 @@ export class TestRunner implements vscode.Disposable { let testsToRun = request.exclude ? request.include?.filter(x => !request.exclude!.includes(x)) : request.include log.trace("Running tests", testsToRun?.map(x => x.id)); - let command: string if (request.profile.label === 'ResolveTests') { // Load tests - command = this.manager.config.getResolveTestsCommand(testsToRun) - await this.runTestFramework(command, testRun, request.profile) + await this.runTestFramework( + this.manager.config.getResolveTestsCommand(testsToRun), + testRun, + request.profile + ) } else { // Run tests + let command: { command: string, args: string[] } if (!testsToRun) { log.debug("Running all tests") this.manager.controller.items.forEach((item) => { // Mark selected tests as started this.enqueTestAndChildren(item, testRun) }) - command = this.manager.config.getFullTestSuiteCommand(debuggerConfig) + command = { + command: this.manager.config.getFullTestSuiteCommand(debuggerConfig), + args: [] + } } else { log.debug("Running selected tests") - command = this.manager.config.getFullTestSuiteCommand(debuggerConfig) + command = { + command: this.manager.config.getFullTestSuiteCommand(debuggerConfig), + args: [] + } for (const node of testsToRun) { log.trace('Adding test to command: %s', node.id) // Mark selected tests as started @@ -125,14 +134,13 @@ export class TestRunner implements vscode.Disposable { if (this.manager.config.frameworkName() == 'RSpec') { let locationStartIndex = node.id.lastIndexOf('[') + 1 let locationEndIndex = node.id.lastIndexOf(']') - command = `${command} ${node.uri?.fsPath}[${node.id.slice(locationStartIndex, locationEndIndex)}]` + command.args.push(`${node.uri!.fsPath}[${node.id.slice(locationStartIndex, locationEndIndex)}]`) } else { - command = `${command} ${node.uri?.fsPath}:${node.range!.start.line + 1}` + command.args.push(`${node.uri!.fsPath}:${node.range!.start.line + 1}`) } } else { - command = `${command} ${node.uri?.fsPath}` + command.args.push(node.uri!.fsPath) } - log.trace("Current command: %s", command) } } if (debuggerConfig) { @@ -235,7 +243,7 @@ export class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - private async runTestFramework (testCommand: string, testRun: vscode.TestRun, profile: vscode.TestRunProfile): Promise { + private async runTestFramework (testCommand: { command: string, args: string[] }, testRun: vscode.TestRun, profile: vscode.TestRunProfile): Promise { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, @@ -249,7 +257,7 @@ export class TestRunner implements vscode.Disposable { this.log.warn('Test run already in progress for profile kind: %s', testProfileKind) return } - let testProcess = new FrameworkProcess(this.log, testCommand, spawnArgs, testRun.token, this.manager) + let testProcess = new FrameworkProcess(this.log, testCommand.command, spawnArgs, testRun.token, this.manager) this.disposables.push(testProcess) this.testProcessMap.set(testProfileKind, testProcess); @@ -261,7 +269,7 @@ export class TestRunner implements vscode.Disposable { ) this.disposables.push(statusListener) try { - await testProcess.startProcess(this.debugCommandStartedResolver) + await testProcess.startProcess(testCommand.args, this.debugCommandStartedResolver) } finally { this.disposeInstance(statusListener) this.disposeInstance(testProcess) diff --git a/test/suite/unitTests/frameworkProcess.test.ts b/test/suite/unitTests/frameworkProcess.test.ts index 8d84cda..4ad0797 100644 --- a/test/suite/unitTests/frameworkProcess.test.ts +++ b/test/suite/unitTests/frameworkProcess.test.ts @@ -1,12 +1,17 @@ -import { before, beforeEach, afterEach } from 'mocha'; -import { instance, mock, when } from '@typestrong/ts-mockito' +import { after, afterEach, before, beforeEach } from 'mocha'; +import { anything, capture, instance, mock, verify, when } from '@typestrong/ts-mockito' +import { expect } from 'chai'; +import { IChildLogger } from '@vscode-logging/logger'; import * as childProcess from 'child_process'; +import * as fs from 'fs/promises'; +import * as os from 'os'; import * as vscode from 'vscode' import * as path from 'path' import { Config } from "../../../src/config"; import { TestSuiteManager } from "../../../src/testSuiteManager"; import { FrameworkProcess } from '../../../src/frameworkProcess'; +import { Status, TestStatus } from '../../../src/testStatus'; import { logger, testItemCollectionMatches, TestItemExpectation } from "../helpers"; @@ -311,4 +316,243 @@ suite('FrameworkProcess', function () { }) }) }) + + suite('error handling', function() { + let tmpDir: string | undefined + let mockLog: IChildLogger + let mockTestManager: TestSuiteManager + let cancellationTokenSource = new vscode.CancellationTokenSource() + + before(async function() { + // Create temp dir in which to put scripts + try { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ruby-test-adapter-')); + } catch (err) { + console.error(err); + } + }); + + after(async function() { + // Delete temp dir and all that's in it + if (tmpDir) { + try { + await fs.rm(tmpDir, { recursive: true, force: true }); + } catch (err) { + console.error(err); + } + } + }) + + beforeEach(function() { + mockLog = mock() + when(mockLog.getChildLogger(anything())).thenReturn(instance(mockLog)) + mockTestManager = mock() + }); + + let echo = (content: string, channel: string = 'stdout'): string => { + let command = 'echo' + if (os.platform() == 'win32' && content.length == 0) { + command = `${command}.` + } else { + command = `${command} "${content}"` + } + if (channel == 'stderr') { + return `${command} 1>&2` + } + return command + } + + let shellCommand = (): string => { + if (os.platform() == 'win32') { + return 'cmd' + } else { + return 'sh' + } + } + + let scriptName = (name: string): string => { + if (os.platform() == 'win32') { + return `${name}.bat` + } else { + return `${name}.sh` + } + } + + let runCommand = async ( + name: string, + content: string[], + statusListener?: (e: TestStatus) => any, + exitCode: number = 0, + onDebugStarted?: () => any) => { + if (tmpDir) { + let script = scriptName(name) + let scriptPath = path.join(tmpDir, script) + let scriptFile = await fs.open(scriptPath, 'w+') + try { + content.push(`exit ${exitCode}`) + await scriptFile.writeFile(content.join("\n")) + await scriptFile.sync() + } finally { + await scriptFile.close() + } + + let command = shellCommand() + + let spawnArgs = { + cwd: tmpDir, + env: process.env + } + + let fp = new FrameworkProcess( + instance(mockLog), command, spawnArgs, cancellationTokenSource.token, instance(mockTestManager) + ) + let listenerDisposable: vscode.Disposable | undefined = undefined + if (statusListener) { + listenerDisposable = fp.testStatusEmitter.event(statusListener) + } + try { + return await fp.startProcess([script], onDebugStarted) + } finally { + if (listenerDisposable) { + listenerDisposable.dispose() + } + fp.dispose() + } + } else { + throw new Error("Missing temp directory for test script") + } + } + + suite('when command has a non-zero exit code', function() { + test('with stdout message', async function() { + const message = [ + 'The tests will not work :(', + 'You won\'t go to space today', + 'Install dependencies' + ] + let statusMessageCount = 0; + let exitCode = 1; + + try { + await runCommand('non-zero-stdout', message.map(x => echo(x)), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + const [logMessage, logData] = capture(mockLog.error).last(); + expect(logMessage).to.eq('Test process failed to run') + expect(logData).to.eql({ message: message }) + expect(statusMessageCount).to.eq(0) + }); + + test('with stderr message', async function() { + const message = [ + 'What happen?', + 'Someone set us up the bomb!', + 'We get signal', + 'Main screen turn on' + ] + let statusMessageCount = 0; + let exitCode = 10; + + try { + await runCommand('non-zero-stderr', message.map(x => echo(x, 'stderr')), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + const [logMessage, logData] = capture(mockLog.error).last(); + expect(logMessage).to.eq('Test process failed to run') + expect(logData).to.eql({ message: message }) + expect(statusMessageCount).to.eq(0) + }); + }); + + suite('when command has a zero exit code', function() { + test('with stdout message', async function() { + const messages = [ + 'This should be ignored', + 'RUNNING: with scissors', + 'PANIC: at the disco', + 'PASSED: in the roller rink' + ] + let statusMessageCount = 0; + let exitCode = 0; + let testItem = testController.createTestItem("with scissors", "with scissors") + when(mockTestManager.getOrCreateTestItem(anything())).thenReturn(testItem) + + try { + await runCommand('zero-stdout', messages.map(x => echo(x)), (_) => {statusMessageCount++}, exitCode) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + verify(mockLog.error(anything(), anything())).times(0) + verify(mockLog.info(anything(), anything())).times(2) + const [logMessage1, logData1] = capture(mockLog.info).first(); + const [logMessage2, logData2] = capture(mockLog.info).last(); + + expect(logMessage1).to.eq('Test run started - stopped capturing error output') + expect(logData1).to.eql({ event: new TestStatus(testItem, Status.running) }) + expect(logMessage2).to.eq('stdout: %s') + expect(logData2).to.eql('PANIC: at the disco') + expect(statusMessageCount).to.eq(2) + }); + + test('with stderr message', async function() { + const stdoutMessages = [ + 'This should be ignored', + 'RUNNING: with scissors', + 'PANIC: at the disco', + 'PASSED: in the roller rink' + ] + const stderrMessage = "Fast Debugger - the debugger of tomorrow, today!" + + let statusMessageCount = 0; + let debugStarted = false + let exitCode = 0; + let testItem = testController.createTestItem("with scissors", "with scissors") + when(mockTestManager.getOrCreateTestItem(anything())).thenReturn(testItem) + + const scriptContent = [echo(stderrMessage, 'stderr')] + for (const line of stdoutMessages) scriptContent.push(echo(line)) + try { + await runCommand('zero-stdout', scriptContent, (_) => {statusMessageCount++}, exitCode, () => {debugStarted = true}) + } catch (err) { + expect((err as Error).message).to.eq(`Child process exited abnormally. Status code: ${exitCode}`) + } + + verify(mockLog.error(anything(), anything())).times(0) + verify(mockLog.info(anything())).times(1) + verify(mockLog.info(anything(), anything())).times(2) + const [logMessage1, logData1] = capture(mockLog.info).first(); + const [logMessage2, logData2] = capture(mockLog.info).second(); + const [logMessage3, logData3] = capture(mockLog.info).last(); + let logMessages = [logMessage1, logMessage2, logMessage3] + let logData = [logData1, logData2, logData3] + + let expectedLogMessages = [ + 'Notifying debug session that test process is ready to debug', + 'Test run started - stopped capturing error output', + 'stdout: %s' + ] + for (const logMessage of logMessages) { + expect(expectedLogMessages.includes(logMessage)) + .to.eq(true, `log message "${logMessage}" not found`) + } + for (const data of logData) { + if (typeof data == "string") { + expect(data).to.eq('PANIC: at the disco') + } else if (data) { + expect(data).to.eql({ event: new TestStatus(testItem, Status.running) }) + } + } + expect(expectedLogMessages.includes(logMessage2)).to.eq(true, 'second log message') + expect(expectedLogMessages.includes(logMessage3)).to.eq(true, 'third log message') + + expect(statusMessageCount).to.eq(2) + expect(debugStarted).to.eq(true) + }); + }); + }) }) From 82d080c305380dad49b3a10ecba2adad3dc5ab4b Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 18 Jan 2023 11:11:13 +0000 Subject: [PATCH 102/108] Apply RSpec pattern symlink fix from #115 --- src/rspec/rspecConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 935ca33..f5fab48 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -44,7 +44,7 @@ export class RspecConfig extends Config { public getTestCommandWithFilePattern(): string { let command: string = this.getTestCommand() const dir = this.getRelativeTestDirectory().replace(/\/$/, ""); - let pattern = this.getFilePattern().map(p => `${dir}/**/${p}`).join(',') + let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') return `${command} --pattern '${pattern}'`; } From 06eca34aa68be8ed11fd23c09fd075fdd29a4428 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 18 Jan 2023 11:15:54 +0000 Subject: [PATCH 103/108] Update test for symlink pattern fix --- test/suite/unitTests/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index f1a7496..c688d18 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -59,7 +59,7 @@ suite('Config', function() { .thenReturn(configSection as vscode.WorkspaceConfiguration) let config = new RspecConfig(path.resolve('ruby')) expect(config.getTestCommandWithFilePattern()).to - .eq("bundle exec rspec --pattern 'spec/**/*_test.rb,spec/**/test_*.rb'") + .eq("bundle exec rspec --pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'") }) suite("#getRelativeTestDirectory()", function() { From e150bbc68625afdb637de2a31f0ec3f2eba1b23d Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Wed, 18 Jan 2023 13:24:58 +0000 Subject: [PATCH 104/108] Tidy up command and argument handling --- src/config.ts | 22 +++++++-------------- src/minitest/minitestConfig.ts | 25 +++++++++++++---------- src/rspec/rspecConfig.ts | 33 ++++++++++++++++--------------- src/testRunner.ts | 36 ++++++++++------------------------ 4 files changed, 49 insertions(+), 67 deletions(-) diff --git a/src/config.ts b/src/config.ts index dd30b73..509d775 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,34 +58,26 @@ export abstract class Config { } /** - * Gets the command to run a single spec/test + * Gets the arguments to pass to the command from the test items to be run/loaded * - * @param testItem The testItem representing the test to be run + * @param testItem[] Array of test items to be run * @param debugConfiguration debug configuration */ - public abstract getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string + public abstract getTestArguments(testItems?: readonly vscode.TestItem[]): string[] /** - * Gets the command to run tests in a given file. + * Gets the command to run the test framework. * - * @param testItem The testItem representing the file to be run * @param debugConfiguration debug configuration */ - public abstract getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string + public abstract getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string /** - * Gets the command to run the full test suite for the current workspace. - * - * @param debugConfiguration debug configuration - */ - public abstract getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string - - /** - * Gets the command to resolve some or all of the tests in the suite + * Gets the command to load some or all of the tests in the suite * * @param testItems Array of TestItems to resolve children of, or undefined to resolve all tests */ - public abstract getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } + public abstract getResolveTestsCommand(): string /** * Get the env vars to run the subprocess with. diff --git a/src/minitest/minitestConfig.ts b/src/minitest/minitestConfig.ts index 9d9c3ac..5c1407b 100644 --- a/src/minitest/minitestConfig.ts +++ b/src/minitest/minitestConfig.ts @@ -57,21 +57,26 @@ export class MinitestConfig extends Config { || path.join('.', 'test'); } - public getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - let line = testItem.range!.start.line + 1 - return `${this.testCommandWithDebugger(debugConfiguration)} '${testItem.uri?.fsPath}:${line}'` - }; + public getTestArguments(testItems?: readonly vscode.TestItem[]): string[] { + if (!testItems) return [] - public getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - return `${this.testCommandWithDebugger(debugConfiguration)} '${testItem.uri?.fsPath}'` - }; + let args: string[] = [] + for (const testItem of testItems) { + if (testItem.id.includes('[')) { + args.push(`${testItem.uri!.fsPath}:${testItem.range!.start.line + 1}`) + } else { + args.push(testItem.uri!.fsPath) + } + } + return args + } - public getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string { + public getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string { return this.testCommandWithDebugger(debugConfiguration) }; - public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } { - return { command: `${this.getTestCommand()} vscode:minitest:list`, args: [] } + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + return `${this.getTestCommand()} vscode:minitest:list` } /** diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index f5fab48..5257081 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -72,27 +72,28 @@ export class RspecConfig extends Config { return cmd } - public getSingleTestCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - return `${this.testCommandWithFormatterAndDebugger(debugConfiguration)} '${this.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` - }; + public getTestArguments(testItems?: readonly vscode.TestItem[]): string[] { + if (!testItems) return [] - public getTestFileCommand(testItem: vscode.TestItem, debugConfiguration?: vscode.DebugConfiguration): string { - return `${this.testCommandWithFormatterAndDebugger(debugConfiguration)} '${this.getAbsoluteTestDirectory()}${path.sep}${testItem.id}'` - }; + let args: string[] = [] + for (const testItem of testItems) { + if (testItem.id.includes('[')) { + let locationStartIndex = testItem.id.lastIndexOf('[') + 1 + let locationEndIndex = testItem.id.lastIndexOf(']') + args.push(`${testItem.uri!.fsPath}[${testItem.id.slice(locationStartIndex, locationEndIndex)}]`) + } else { + args.push(testItem.uri!.fsPath) + } + } + return args + } - public getFullTestSuiteCommand(debugConfiguration?: vscode.DebugConfiguration): string { + public getRunTestsCommand(debugConfiguration?: vscode.DebugConfiguration): string { return this.testCommandWithFormatterAndDebugger(debugConfiguration) }; - public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): { command: string, args: string[] } { - let cmd = `${this.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; - - let args: string[] = [] - testItems?.forEach((item) => { - let testPath = path.join(this.getAbsoluteTestDirectory(), item.id) - args.push(testPath) - }) - return { command: cmd, args: args } + public getResolveTestsCommand(testItems?: readonly vscode.TestItem[]): string { + return `${this.testCommandWithFormatterAndDebugger()} --order defined --dry-run`; } /** diff --git a/src/testRunner.ts b/src/testRunner.ts index aecd15d..ddab092 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -103,45 +103,29 @@ export class TestRunner implements vscode.Disposable { if (request.profile.label === 'ResolveTests') { // Load tests await this.runTestFramework( - this.manager.config.getResolveTestsCommand(testsToRun), + { + command: this.manager.config.getResolveTestsCommand(), + args: this.manager.config.getTestArguments(testsToRun), + }, testRun, request.profile ) } else { // Run tests - let command: { command: string, args: string[] } if (!testsToRun) { log.debug("Running all tests") this.manager.controller.items.forEach((item) => { // Mark selected tests as started this.enqueTestAndChildren(item, testRun) }) - command = { - command: this.manager.config.getFullTestSuiteCommand(debuggerConfig), - args: [] - } } else { log.debug("Running selected tests") - command = { - command: this.manager.config.getFullTestSuiteCommand(debuggerConfig), - args: [] - } - for (const node of testsToRun) { - log.trace('Adding test to command: %s', node.id) - // Mark selected tests as started - this.enqueTestAndChildren(node, testRun) - if (node.id.includes('[')) { - if (this.manager.config.frameworkName() == 'RSpec') { - let locationStartIndex = node.id.lastIndexOf('[') + 1 - let locationEndIndex = node.id.lastIndexOf(']') - command.args.push(`${node.uri!.fsPath}[${node.id.slice(locationStartIndex, locationEndIndex)}]`) - } else { - command.args.push(`${node.uri!.fsPath}:${node.range!.start.line + 1}`) - } - } else { - command.args.push(node.uri!.fsPath) - } - } + // Mark selected tests as started + testsToRun.forEach(item => this.enqueTestAndChildren(item, testRun)) + } + let command = { + command: this.manager.config.getRunTestsCommand(debuggerConfig), + args: this.manager.config.getTestArguments(testsToRun) } if (debuggerConfig) { log.debug('Debugging tests', request.include?.map(x => x.id)); From edf29169793106ffe633d79628862ea680f406c1 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 24 Apr 2023 21:07:50 +0100 Subject: [PATCH 105/108] Update dependencies with `npm audit fix` --- package-lock.json | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bace9d..e2e932e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,9 +161,9 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.16.0.tgz", - "integrity": "sha512-BhJ0zO7UxShLFBZM6jwOLt1ZVoqQ4r5Lj/kHNeYp0ICPXhz/erqBSMQnHkRgkjn2L/bh+TYFGkZyguhu/SKsjw==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", "dev": true, "dependencies": { "azure-devops-node-api": "^11.0.1", @@ -172,6 +172,7 @@ "commander": "^6.1.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^12.3.2", "mime": "^1.3.4", @@ -182,7 +183,7 @@ "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", - "xml2js": "^0.4.23", + "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, @@ -1623,6 +1624,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2968,9 +2975,9 @@ "dev": true }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", @@ -3198,9 +3205,9 @@ } }, "@vscode/vsce": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.16.0.tgz", - "integrity": "sha512-BhJ0zO7UxShLFBZM6jwOLt1ZVoqQ4r5Lj/kHNeYp0ICPXhz/erqBSMQnHkRgkjn2L/bh+TYFGkZyguhu/SKsjw==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", "dev": true, "requires": { "azure-devops-node-api": "^11.0.1", @@ -3209,6 +3216,7 @@ "commander": "^6.1.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", "keytar": "^7.7.0", "leven": "^3.1.0", "markdown-it": "^12.3.2", @@ -3220,7 +3228,7 @@ "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", - "xml2js": "^0.4.23", + "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, @@ -4293,6 +4301,12 @@ "argparse": "^2.0.1" } }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -5341,9 +5355,9 @@ "dev": true }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", From 235ad1df802a3b5f23096e571ba079a1e370d5cc Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 24 Apr 2023 22:19:24 +0100 Subject: [PATCH 106/108] Stop using TestRunProfile for loading tests Fixes the issue of test loads blocking test runs - now the two can happen in parallel --- src/loaderQueue.ts | 2 +- src/main.ts | 13 ++----------- src/testFactory.ts | 5 ++--- src/testLoader.ts | 9 ++++++--- src/testRunner.ts | 24 +++++++++--------------- src/testStatusListener.ts | 17 +++++------------ test/suite/minitest/minitest.test.ts | 7 +------ test/suite/rspec/rspec.test.ts | 7 +------ 8 files changed, 27 insertions(+), 57 deletions(-) diff --git a/src/loaderQueue.ts b/src/loaderQueue.ts index 37bd7ee..fbd5690 100644 --- a/src/loaderQueue.ts +++ b/src/loaderQueue.ts @@ -111,7 +111,7 @@ export class LoaderQueue implements vscode.Disposable { this.notifyQueueWorker = undefined this.terminateQueueWorker = undefined } else { - // Drain queue to get batch of test items to load + // Drain queue to get batch of test items to process let queueItems = Array.from(this.queue) this.queue.clear() diff --git a/src/main.ts b/src/main.ts index b4c025e..b9c2427 100644 --- a/src/main.ts +++ b/src/main.ts @@ -66,7 +66,7 @@ export async function activate(context: vscode.ExtensionContext) { const controller = vscode.tests.createTestController('ruby-test-explorer', 'Ruby Test Explorer'); // TODO: (?) Add a "Profile" profile for profiling tests - const profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { + const profiles: { runProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile } = { // Default run profile for running tests runProfile: controller.createRunProfile( 'Run', @@ -75,14 +75,6 @@ export async function activate(context: vscode.ExtensionContext) { true // Default run profile ), - // Run profile for dry runs/getting test details - resolveTestsProfile: controller.createRunProfile( - 'ResolveTests', - vscode.TestRunProfileKind.Run, - (request, token) => factory.getRunner().runHandler(request, token), - false - ), - // Run profile for debugging tests debugProfile: controller.createRunProfile( 'Debug', @@ -92,13 +84,12 @@ export async function activate(context: vscode.ExtensionContext) { ), } - const factory = new TestFactory(log, controller, testConfig, profiles, workspace); + const factory = new TestFactory(log, controller, testConfig, workspace); // Ensure disposables are registered with VSC to be disposed of when the extension is deactivated context.subscriptions.push(controller); context.subscriptions.push(profiles.runProfile); context.subscriptions.push(profiles.debugProfile); - context.subscriptions.push(profiles.resolveTestsProfile); context.subscriptions.push(factory); controller.resolveHandler = async test => { diff --git a/src/testFactory.ts b/src/testFactory.ts index f3825a1..63ad40d 100644 --- a/src/testFactory.ts +++ b/src/testFactory.ts @@ -25,7 +25,6 @@ export class TestFactory implements vscode.Disposable { private readonly rootLog: IVSCodeExtLogger, private readonly controller: vscode.TestController, private config: Config, - private readonly profiles: { runProfile: vscode.TestRunProfile, resolveTestsProfile: vscode.TestRunProfile, debugProfile: vscode.TestRunProfile }, private readonly workspace?: vscode.WorkspaceFolder, ) { this.log = rootLog.getChildLogger({ label: `${TestFactory.name}` }) @@ -83,8 +82,8 @@ export class TestFactory implements vscode.Disposable { if (!this.loader) { this.loader = new TestLoader( this.rootLog, - this.profiles.resolveTestsProfile, - this.manager + this.manager, + this.getRunner() ) this.disposables.push(this.loader) } diff --git a/src/testLoader.ts b/src/testLoader.ts index f07bcdf..3b47382 100644 --- a/src/testLoader.ts +++ b/src/testLoader.ts @@ -3,6 +3,7 @@ import path from 'path' import { IChildLogger } from '@vscode-logging/logger'; import { TestSuiteManager } from './testSuiteManager'; import { LoaderQueue } from './loaderQueue'; +import { TestRunner } from './testRunner'; /** * Responsible for finding and watching test files, and loading tests from within those @@ -15,17 +16,19 @@ export class TestLoader implements vscode.Disposable { private readonly log: IChildLogger; private readonly cancellationTokenSource = new vscode.CancellationTokenSource() private readonly resolveQueue: LoaderQueue + private readonly runHandler: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => Thenable constructor( readonly rootLog: IChildLogger, - private readonly resolveTestProfile: vscode.TestRunProfile, private readonly manager: TestSuiteManager, + private readonly testRunner: TestRunner ) { this.log = rootLog.getChildLogger({ label: 'TestLoader' }); this.resolveQueue = new LoaderQueue(rootLog, async (testItems?: vscode.TestItem[]) => await this.loadTests(testItems)) this.disposables.push(this.cancellationTokenSource) this.disposables.push(this.resolveQueue) this.disposables.push(this.configWatcher()); + this.runHandler = (request, token) => this.testRunner.runHandler(request, token) } dispose(): void { @@ -123,8 +126,8 @@ export class TestLoader implements vscode.Disposable { log.info('Loading tests...', { testIds: testItems?.map(x => x.id) || 'all tests' }); try { if (testItems) { for (const item of testItems) { item.busy = true }} - let request = new vscode.TestRunRequest(testItems, undefined, this.resolveTestProfile) - await this.resolveTestProfile.runHandler(request, this.cancellationTokenSource.token) + let request = new vscode.TestRunRequest(testItems) + await this.runHandler(request, this.cancellationTokenSource.token) } catch (e: any) { log.error('Failed to load tests', e) return Promise.reject(e) diff --git a/src/testRunner.ts b/src/testRunner.ts index ddab092..9b046cb 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -10,7 +10,7 @@ export class TestRunner implements vscode.Disposable { protected debugCommandStartedResolver?: () => void; protected disposables: { dispose(): void }[] = []; protected readonly log: IChildLogger; - private readonly testProcessMap: Map + private readonly testProcessMap: Map /** * @param rootLog The Test Adapter logger, for logging. @@ -83,11 +83,6 @@ export class TestRunner implements vscode.Disposable { ) { let log = this.log.getChildLogger({ label: 'runHandler' }) - if (!request.profile) { - log.error('Test run request is missing a profile', {request: request}) - return - } - // Loop through all included tests, or all known tests, and add them to our queue log.debug('Number of tests in request', request.include?.length || 0); @@ -100,15 +95,14 @@ export class TestRunner implements vscode.Disposable { let testsToRun = request.exclude ? request.include?.filter(x => !request.exclude!.includes(x)) : request.include log.trace("Running tests", testsToRun?.map(x => x.id)); - if (request.profile.label === 'ResolveTests') { + if (!request.profile) { // Load tests await this.runTestFramework( { command: this.manager.config.getResolveTestsCommand(), args: this.manager.config.getTestArguments(testsToRun), }, - testRun, - request.profile + testRun ) } else { // Run tests @@ -227,7 +221,7 @@ export class TestRunner implements vscode.Disposable { * @param context Test run context for the cancellation token * @returns Raw output from process */ - private async runTestFramework (testCommand: { command: string, args: string[] }, testRun: vscode.TestRun, profile: vscode.TestRunProfile): Promise { + private async runTestFramework (testCommand: { command: string, args: string[] }, testRun: vscode.TestRun, profile?: vscode.TestRunProfile): Promise { const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace?.uri.fsPath, shell: true, @@ -235,7 +229,7 @@ export class TestRunner implements vscode.Disposable { }; this.log.info('Running command: %s', testCommand); - let testProfileKind = profile.kind + let testProfileKind = profile?.kind || null if (this.testProcessMap.get(testProfileKind)) { this.log.warn('Test run already in progress for profile kind: %s', testProfileKind) @@ -247,9 +241,9 @@ export class TestRunner implements vscode.Disposable { const statusListener = TestStatusListener.listen( this.rootLog, - profile, testRun, - testProcess.testStatusEmitter + testProcess.testStatusEmitter, + profile ) this.disposables.push(statusListener) try { @@ -265,8 +259,8 @@ export class TestRunner implements vscode.Disposable { * Terminates the current test run process for the given profile kind if there is one * @param profile The profile to kill the test run for */ - private killTestRun(profile: vscode.TestRunProfile) { - let profileKind = profile.kind + private killTestRun(profile?: vscode.TestRunProfile) { + let profileKind = profile?.kind || null let process = this.testProcessMap.get(profileKind) if (process) { this.disposeInstance(process) diff --git a/src/testStatusListener.ts b/src/testStatusListener.ts index 3591040..604ba8e 100644 --- a/src/testStatusListener.ts +++ b/src/testStatusListener.ts @@ -8,11 +8,11 @@ import { Status, TestStatus } from './testStatus' export class TestStatusListener { public static listen( rootLog: IChildLogger, - profile: vscode.TestRunProfile, testRun: vscode.TestRun, - testStatusEmitter: vscode.EventEmitter + testStatusEmitter: vscode.EventEmitter, + profile?: vscode.TestRunProfile, ): vscode.Disposable { - let log = rootLog.getChildLogger({ label: `${TestStatusListener.name}(${profile.label})`}) + let log = rootLog.getChildLogger({ label: `${TestStatusListener.name}(${profile?.label || "LoadTests"})`}) return testStatusEmitter.event((event: TestStatus) => { switch(event.status) { @@ -21,7 +21,7 @@ export class TestStatusListener { testRun.skipped(event.testItem) break; case Status.passed: - if (this.isTestLoad(profile)) { + if (!profile) { log.info('Test loaded: %s (duration: %d)', event.testItem.id, event.duration) } else { log.info('Test passed: %s (duration: %d)', event.testItem.id, event.duration) @@ -41,7 +41,7 @@ export class TestStatusListener { } break; case Status.running: - if (this.isTestLoad(profile)) { + if (!profile) { log.debug('Ignored test started event from test load: %s (duration: %d)', event.testItem.id, event.duration) } else { log.info('Test started: %s', event.testItem.id) @@ -53,11 +53,4 @@ export class TestStatusListener { } }) } - - /** - * Checks if the current test run is for loading tests rather than running them - */ - private static isTestLoad(profile: vscode.TestRunProfile): boolean { - return profile.label == 'ResolveTests' - } } diff --git a/test/suite/minitest/minitest.test.ts b/test/suite/minitest/minitest.test.ts index db1f0cd..0050c4b 100644 --- a/test/suite/minitest/minitest.test.ts +++ b/test/suite/minitest/minitest.test.ts @@ -27,7 +27,6 @@ suite('Extension Test for Minitest', function() { let testRunner: TestRunner; let testLoader: TestLoader; let manager: TestSuiteManager; - let resolveTestsProfile: vscode.TestRunProfile; let mockTestRun: vscode.TestRun; let cancellationTokenSource: vscode.CancellationTokenSource; @@ -77,10 +76,6 @@ suite('Extension Test for Minitest', function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('minitestDirectory', 'test') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_test.rb']) config = new MinitestConfig(path.resolve("ruby"), workspaceFolder) - let mockProfile = mock() - when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) - when(mockProfile.label).thenReturn('ResolveTests') - resolveTestsProfile = instance(mockProfile) testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') mockTestRun = mock() @@ -93,7 +88,7 @@ suite('Extension Test for Minitest', function() { manager = new TestSuiteManager(log, testController, config) testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); + testLoader = new TestLoader(log, manager, testRunner); }) beforeEach(function() { diff --git a/test/suite/rspec/rspec.test.ts b/test/suite/rspec/rspec.test.ts index ab91618..675b72f 100644 --- a/test/suite/rspec/rspec.test.ts +++ b/test/suite/rspec/rspec.test.ts @@ -27,7 +27,6 @@ suite('Extension Test for RSpec', function() { let testRunner: TestRunner; let testLoader: TestLoader; let manager: TestSuiteManager; - let resolveTestsProfile: vscode.TestRunProfile; let mockTestRun: vscode.TestRun; let cancellationTokenSource: vscode.CancellationTokenSource; @@ -90,10 +89,6 @@ suite('Extension Test for RSpec', function() { vscode.workspace.getConfiguration('rubyTestExplorer').update('rspecDirectory', 'spec') vscode.workspace.getConfiguration('rubyTestExplorer').update('filePattern', ['*_spec.rb']) config = new RspecConfig(path.resolve("ruby"), workspaceFolder) - let mockProfile = mock() - when(mockProfile.runHandler).thenReturn((request, token) => testRunner.runHandler(request, token)) - when(mockProfile.label).thenReturn('ResolveTests') - resolveTestsProfile = instance(mockProfile) testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') mockTestRun = mock() @@ -106,7 +101,7 @@ suite('Extension Test for RSpec', function() { manager = new TestSuiteManager(log, testController, config) testRunner = new TestRunner(log, manager, workspaceFolder) - testLoader = new TestLoader(log, resolveTestsProfile, manager); + testLoader = new TestLoader(log, manager, testRunner); }) beforeEach(function() { From 7a0982cac48f95a3824bde7b612e89963b568902 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Mon, 24 Apr 2023 23:14:22 +0100 Subject: [PATCH 107/108] Fix markdown lint warning in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b5d1b57..80c3d39 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Ruby Test Explorer + **[Install it from the VS Code Marketplace.](https://marketplace.visualstudio.com/items?itemName=connorshea.vscode-ruby-test-adapter)** This is a Ruby Test Explorer extension for the [VS Code Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer) extension. From 2b4fde9ce353efafc0316a6b6d305121d775cc37 Mon Sep 17 00:00:00 2001 From: Tabby Cromarty Date: Tue, 25 Apr 2023 00:08:57 +0100 Subject: [PATCH 108/108] Use file pattern when running all tests with rspec --- src/frameworkProcess.ts | 6 ++--- src/rspec/rspecConfig.ts | 7 +++--- src/testRunner.ts | 2 +- test/suite/unitTests/config.test.ts | 36 ++++++++++++++++++++++++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/frameworkProcess.ts b/src/frameworkProcess.ts index 69f3ee2..6ccb2b3 100644 --- a/src/frameworkProcess.ts +++ b/src/frameworkProcess.ts @@ -185,7 +185,7 @@ export class FrameworkProcess implements vscode.Disposable { private parseAndHandleTestOutput(testOutput: string): void { let log = this.log.getChildLogger({label: this.parseAndHandleTestOutput.name}) testOutput = this.getJsonFromOutput(testOutput); - log.trace('Parsing the below JSON:', testOutput); + log.trace('Parsing the below JSON: %s', testOutput); let testMetadata = JSON.parse(testOutput); let tests: Array = testMetadata.examples; @@ -198,10 +198,10 @@ export class FrameworkProcess implements vscode.Disposable { let testItem = this.testManager.getOrCreateTestItem(test.id, (item) => { if (item.id == test.id) itemAlreadyExists = false }) testItem.canResolveChildren = !test.id.endsWith(']') - log.trace('canResolveChildren', test.id, testItem.canResolveChildren) + log.trace('canResolveChildren (%s): %s', test.id, testItem.canResolveChildren) testItem.label = this.parseDescription(test) - log.trace('label', test.id, testItem.description) + log.trace('label (%s): %s', test.id, testItem.description) testItem.range = this.parseRange(test) diff --git a/src/rspec/rspecConfig.ts b/src/rspec/rspecConfig.ts index 5257081..4ce6045 100644 --- a/src/rspec/rspecConfig.ts +++ b/src/rspec/rspecConfig.ts @@ -41,11 +41,10 @@ export class RspecConfig extends Config { * * @return The RSpec command */ - public getTestCommandWithFilePattern(): string { - let command: string = this.getTestCommand() + public getFilePatternArg(): string { const dir = this.getRelativeTestDirectory().replace(/\/$/, ""); let pattern = this.getFilePattern().map(p => `${dir}/**{,/*/**}/${p}`).join(',') - return `${command} --pattern '${pattern}'`; + return `--pattern '${pattern}'`; } /** @@ -73,7 +72,7 @@ export class RspecConfig extends Config { } public getTestArguments(testItems?: readonly vscode.TestItem[]): string[] { - if (!testItems) return [] + if (!testItems) return [this.getFilePatternArg()] let args: string[] = [] for (const testItem of testItems) { diff --git a/src/testRunner.ts b/src/testRunner.ts index 9b046cb..d713d1e 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -228,7 +228,7 @@ export class TestRunner implements vscode.Disposable { env: this.manager.config.getProcessEnv() }; - this.log.info('Running command: %s', testCommand); + this.log.info('Running command: %s, args: [%s]', testCommand.command, testCommand.args.join(',')); let testProfileKind = profile?.kind || null if (this.testProcessMap.get(testProfileKind)) { diff --git a/test/suite/unitTests/config.test.ts b/test/suite/unitTests/config.test.ts index c688d18..18ff6f5 100644 --- a/test/suite/unitTests/config.test.ts +++ b/test/suite/unitTests/config.test.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; import { spy, when } from '@typestrong/ts-mockito' +import { after, before } from 'mocha'; import * as vscode from 'vscode' import * as path from 'path' @@ -53,13 +54,13 @@ suite('Config', function() { } } - test("#getTestCommandWithFilePattern", function() { + test("#getFilePatternArg", function() { let spiedWorkspace = spy(vscode.workspace) when(spiedWorkspace.getConfiguration('rubyTestExplorer', null)) .thenReturn(configSection as vscode.WorkspaceConfiguration) let config = new RspecConfig(path.resolve('ruby')) - expect(config.getTestCommandWithFilePattern()).to - .eq("bundle exec rspec --pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'") + expect(config.getFilePatternArg()).to + .eq("--pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'") }) suite("#getRelativeTestDirectory()", function() { @@ -75,5 +76,34 @@ suite('Config', function() { expect(config.getAbsoluteTestDirectory()).to.eq(path.resolve('spec')) }) }) + + suite('#getTestArguments', function() { + let testController: vscode.TestController + + before(function() { + testController = vscode.tests.createTestController('ruby-test-explorer-tests', 'Ruby Test Explorer') + }) + + after(function() { + testController.dispose() + }) + + test('with no args (run full suite)', function() { + let config = new RspecConfig(path.resolve('ruby')) + expect(config.getTestArguments()).to + .eql(["--pattern 'spec/**{,/*/**}/*_test.rb,spec/**{,/*/**}/test_*.rb'"]) + }) + + test('with file arg', function() { + let config = new RspecConfig(path.resolve('ruby')) + let testId = "test_spec.rb" + let testItem = testController.createTestItem( + testId, + "label", + vscode.Uri.file(path.resolve(config.getAbsoluteTestDirectory(), testId)) + ) + expect(config.getTestArguments([testItem])).to.eql([path.resolve('spec/test_spec.rb')]) + }) + }) }) });