diff --git a/README.md b/README.md index 97ad7bf..549c327 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,17 @@ The extension needs Ruby and the `rspec-core` gem installed (and any other depen By default, you need to have `rspec` installed via Bundler with a `Gemfile` and `bundle install`, otherwise `bundle exec rspec` won't work. If you want to run your Rspec tests with a command other than `bundle exec rspec`, you can configure the command with the `rubyTestExplorer.rspecCommand` setting. +If you use Spring, set `rubyTestExplorer.rspecCommand` to `spring rspec` for normal test runs. Debug runs can use `rubyTestExplorer.rspecDebugCommand` and `rubyTestExplorer.debuggerConfig` when the default `rdebug-ide` wrapper is not flexible enough, for example: + +```json +"rubyTestExplorer.rspecDebugCommand": "bundle exec rdbg --open --host ${host} --port ${port} -c -- spring rspec ${formatterArgs}", +"rubyTestExplorer.debuggerConfig": { + "type": "rdbg", + "request": "attach", + "debugPort": "${host}:${port}" +} +``` + ### Minitest The extension needs Ruby and the `minitest` gem installed (and any other dependencies required by your test suite). It's been tested with Ruby 2.5 and 2.6, and Minitest 5.x. It should work with most recent versions of Ruby and Minitest. @@ -39,6 +50,8 @@ Currently supported: - Live test status updates as the test suite runs. - File locations for each test. - Configurable RSpec command. +- Configurable RSpec debug command. +- Configurable debug attach settings. - Configurable RSpec `spec/` directory. - Configurable Minitest command. - Configurable Minitest `test/` directory. @@ -59,7 +72,9 @@ Property | Description `rubyTestExplorer.debuggerHost` | Define the host to connect the debugger to, for example `127.0.0.1`. `rubyTestExplorer.debuggerPort` | Define the port to connect the debugger to, for example `1234`. `rubyTestExplorer.debugCommand` | Define how to run rdebug-ide, for example `rdebug-ide` or `bundle exec rdebug-ide`. +`rubyTestExplorer.debuggerConfig` | Optionally override the VS Code debug configuration used to attach to the test process. String values support `${host}` and `${port}` placeholders. Useful for alternative debuggers such as `rdbg`. `rubyTestExplorer.rspecCommand` | Define the command to run RSpec tests with, for example `bundle exec rspec`, `spring rspec`, or `rspec`. +`rubyTestExplorer.rspecDebugCommand` | Optionally define the full command used to debug RSpec tests. Supports `${debugCommand}`, `${host}`, `${port}`, `${debugPort}`, `${rubyScriptsDir}`, and `${formatterArgs}` placeholders. Useful for Spring or rdbg setups. `rubyTestExplorer.rspecDirectory` | Define the relative directory of the specs in a given workspace, for example `./spec/`. `rubyTestExplorer.minitestCommand` | Define how to run Minitest with Rake, for example `./bin/rake`, `bundle exec rake` or `rake`. Must be a Rake command. `rubyTestExplorer.minitestDirectory` | Define the relative location of your `test` directory, for example `./test/`. diff --git a/package.json b/package.json index 707db55..67f2d58 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,18 @@ "default": "rdebug-ide", "type": "string", "scope": "resource" + }, + "rubyTestExplorer.debuggerConfig": { + "markdownDescription": "Optionally override the VS Code debug configuration used to attach to the test process. String values support `${host}` and `${port}` placeholders. Useful for alternative debuggers such as `rdbg`.", + "default": {}, + "type": "object", + "scope": "resource" + }, + "rubyTestExplorer.rspecDebugCommand": { + "markdownDescription": "Optionally define the full command used to debug RSpec tests. Supports `${debugCommand}`, `${host}`, `${port}`, `${debugPort}`, `${rubyScriptsDir}`, and `${formatterArgs}` placeholders. Useful for Spring or rdbg setups, for example `bundle exec rdbg --open --host ${host} --port ${port} -c -- spring rspec ${formatterArgs}`.", + "default": "", + "type": "string", + "scope": "resource" } } } diff --git a/src/adapter.ts b/src/adapter.ts index f98c04a..50cec1a 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -73,15 +73,23 @@ export class RubyAdapter implements TestAdapter { this.log.info(`Debugging test(s) ${JSON.stringify(testsToRun)} of ${this.workspace.uri.fsPath}`); const config = vscode.workspace.getConfiguration('rubyTestExplorer', null) + const debuggerHost = config.get('debuggerHost') || "127.0.0.1"; + const debuggerPort = config.get('debuggerPort') || "1234"; - const debuggerConfig = { + const defaultDebuggerConfig = { name: "Debug Ruby Tests", type: "Ruby", request: "attach", - remoteHost: config.get('debuggerHost') || "127.0.0.1", - remotePort: config.get('debuggerPort') || "1234", + remoteHost: debuggerHost, + remotePort: debuggerPort, remoteWorkspaceRoot: "${workspaceRoot}" } + const debuggerConfigOverride = (config.get('debuggerConfig') as any) || {}; + const debuggerConfig = this.interpolateDebugConfig( + Object.assign({}, defaultDebuggerConfig, debuggerConfigOverride), + String(debuggerHost), + String(debuggerPort) + ); const testRunPromise = this.run(testsToRun, debuggerConfig); @@ -187,6 +195,25 @@ export class RubyAdapter implements TestAdapter { } } + protected interpolateDebugConfig(config: any, host: string, port: string): any { + if (typeof config === 'string') { + return config + .replace(/\$\{host\}/g, host) + .replace(/\$\{port\}/g, port); + } + if (Array.isArray(config)) { + return config.map((item) => this.interpolateDebugConfig(item, host, port)); + } + if (config && typeof config === 'object') { + const interpolated: any = {}; + for (const key of Object.keys(config)) { + interpolated[key] = this.interpolateDebugConfig(config[key], host, port); + } + return interpolated; + } + return config; + } + protected async startDebugging(debuggerConfig: vscode.DebugConfiguration): Promise { const debugSessionPromise = new Promise((resolve, reject) => { diff --git a/src/rspecTests.ts b/src/rspecTests.ts index be92348..5ff3a72 100644 --- a/src/rspecTests.ts +++ b/src/rspecTests.ts @@ -38,7 +38,8 @@ export class RspecTests extends Tests { // Allow a buffer of 64MB. const execArgs: childProcess.ExecOptions = { cwd: this.workspace.uri.fsPath, - maxBuffer: 8192 * 8192 + maxBuffer: 8192 * 8192, + env: this.getProcessEnv() }; childProcess.exec(cmd, execArgs, (err, stdout) => { @@ -88,9 +89,23 @@ export class RspecTests extends Tests { * @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'; + const config = vscode.workspace.getConfiguration('rubyTestExplorer', null); + let command: string = (config.get('debugCommand') as string) || 'rdebug-ide'; + let template: string = (config.get('rspecDebugCommand') as string) || ''; + + if (template.trim()) { + return template.replace(/\$\{(\w+)\}/g, (_match, name) => { + const replacements: { [key: string]: string } = { + debugCommand: command, + host: String(debuggerConfig.remoteHost), + port: String(debuggerConfig.remotePort), + debugPort: `${debuggerConfig.remoteHost}:${debuggerConfig.remotePort}`, + rubyScriptsDir: process.platform == 'win32' ? '%EXT_DIR%' : '$EXT_DIR', + formatterArgs: args, + }; + return replacements[name] ?? _match; + }); + } return ( `${command} --host ${debuggerConfig.remoteHost} --port ${debuggerConfig.remotePort}` + @@ -192,7 +207,8 @@ export class RspecTests extends Tests { this.log.info(`Running test file: ${testFile}`); const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace.uri.fsPath, - shell: true + shell: true, + env: this.getProcessEnv() }; // Run tests for a given file at once with a single command. @@ -217,7 +233,8 @@ export class RspecTests extends Tests { this.log.info(`Running full test suite.`); const spawnArgs: childProcess.SpawnOptions = { cwd: this.workspace.uri.fsPath, - shell: true + shell: true, + env: this.getProcessEnv() }; let testCommand = this.testCommandWithFormatterAndDebugger(debuggerConfig); diff --git a/src/tests.ts b/src/tests.ts index e117146..e1448e9 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -366,7 +366,7 @@ export abstract class Tests { 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) { + if (this.isDebugCommandReady(data) && this.debugCommandStartedResolver) { this.debugCommandStartedResolver() } }); @@ -510,6 +510,13 @@ export abstract class Tests { }) } + protected isDebugCommandReady(data: string): boolean { + return data.startsWith('Fast Debugger') + || data.startsWith('DEBUGGER: Debugger can attach') + || data.startsWith('DEBUGGER: wait for debugger connection') + || data.startsWith('DEBUGGER: Connected'); + } + /** * Get the absolute path of the custom_formatter.rb file. *