diff --git a/CHANGELOG.md b/CHANGELOG.md index ea47742..db4df9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add custom debug configurations + ## [0.6.0] - 2020-05-03 ### Added diff --git a/README.md b/README.md index 5f614d3..1164f80 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,43 @@ Run your [CMake](https://cmake.org) tests using the [Test Explorer UI](https://m | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cmakeExplorer.buildDir` | Location of the CMake build directory. Can be absolute or relative to the workspace. Defaults to empty, i.e. the workspace directory. | | `cmakeExplorer.buildConfig` | Name of the CMake build configuration. Can be set to any standard or custom configuration name (e.g. `Default`, `Release`, `RelWithDebInfo`, `MinSizeRel` ). Case-insensitive. Defaults to empty, i.e. no specific configuration. | +| `cmakeExplorer.debugConfig` | Custom debug configuration to use (empty for default). See [Debugging](#debugging) for more info. | | `cmakeExplorer.extraCtestLoadArgs` | Extra command-line arguments passed to CTest at load time. For example, `-R foo` will only load the tests containing the string `foo`. Defaults to empty. | | `cmakeExplorer.extraCtestRunArgs` | Extra command-line arguments passed to CTest at run time. For example, `-V` will enable verbose output from tests. Defaults to empty. | + +## Debugging + +The extension comes pre-configured with sensible defaults for debugging tests: + +```json +{ + "name": "CTest", + "type": "cppdbg", + "request": "launch", + "windows": { + "type": "cppvsdbg" + }, + "linux": { + "type": "cppdbg", + "MIMode": "gdb" + }, + "osx": { + "type": "cppdbg", + "MIMode": "lldb" + } +} +``` + +You can also use a custom configuration defined in the standard `launch.json`. +To do so, edit the `cmakeExplorer.debugConfig` settings with the name of the +debug configuration to use. + +Debugging a test will overwrite the following debug configuration fields with +values from the CTest metadata: + +| Field | Value | +| --------- | -------------------------------- | +| `name` | `CTest ${test name}` | +| `program` | CTest `COMMAND` option | +| `args` | CTest arguments | +| `cwd` | CTest `WORKING_DIRECTORY` option | diff --git a/package-lock.json b/package-lock.json index 693a8bc..ea12a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -399,6 +399,12 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/package.json b/package.json index 2e49906..184a125 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "devDependencies": { "@types/vscode": "~1.23.0", + "prettier": "^2.0.5", "typescript": "^3.5.3", "vsce": "^1.65.0" }, @@ -68,6 +69,12 @@ "default": "", "scope": "resource" }, + "cmakeExplorer.debugConfig": { + "description": "Custom debug configuration to use (empty for default)", + "type": "string", + "default": "", + "scope": "resource" + }, "cmakeExplorer.extraCtestLoadArgs": { "description": "Extra command-line arguments passed to CTest at load time", "type": "string", diff --git a/src/cmake-adapter.ts b/src/cmake-adapter.ts index c2fdde3..fef8dc4 100644 --- a/src/cmake-adapter.ts +++ b/src/cmake-adapter.ts @@ -50,7 +50,10 @@ export class CmakeAdapter implements TestAdapter { 'idle'; /** Currently running test */ - private currentTest?: CmakeTestProcess; + private currentTestProcess?: CmakeTestProcess; + + /** Currently debugged test config */ + private debuggedTestConfig?: Partial; // // TestAdapter implementations @@ -82,10 +85,28 @@ export class CmakeAdapter implements TestAdapter { constructor( public readonly workspaceFolder: vscode.WorkspaceFolder, - private readonly log: Log + private readonly log: Log, + context: vscode.ExtensionContext ) { this.log.info('Initializing CMake test adapter'); + // Register a DebugConfigurationProvider to combine global and + // test-specific debug configurations (see debugTest) + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider('cppdbg', { + resolveDebugConfiguration: ( + folder: vscode.WorkspaceFolder | undefined, + config: vscode.DebugConfiguration, + token?: vscode.CancellationToken + ): vscode.ProviderResult => { + return { + ...config, + ...this.debuggedTestConfig, + }; + }, + }) + ); + this.disposables.push(this.testsEmitter); this.disposables.push(this.testStatesEmitter); this.disposables.push(this.autorunEmitter); @@ -201,7 +222,7 @@ export class CmakeAdapter implements TestAdapter { cancel(): void { if (this.state !== 'running') return; // ignore - if (this.currentTest) cancelCmakeTest(this.currentTest); + if (this.currentTestProcess) cancelCmakeTest(this.currentTestProcess); // State will eventually transition to idle once the run loop completes this.state = 'cancelled'; @@ -250,7 +271,7 @@ export class CmakeAdapter implements TestAdapter { // Single test // - const test = this.cmakeTests.find(test => test.name === id); + const test = this.cmakeTests.find((test) => test.name === id); if (!test) { // Not found, mark test as skipped. this.testStatesEmitter.fire({ @@ -273,12 +294,14 @@ export class CmakeAdapter implements TestAdapter { this.workspaceFolder.uri ); const extraCtestRunArgs = config.get('extraCtestRunArgs') || ''; - this.currentTest = scheduleCmakeTest( + this.currentTestProcess = scheduleCmakeTest( this.ctestPath, test, extraCtestRunArgs ); - const result: CmakeTestResult = await executeCmakeTest(this.currentTest); + const result: CmakeTestResult = await executeCmakeTest( + this.currentTestProcess + ); this.testStatesEmitter.fire({ type: 'test', test: id, @@ -293,7 +316,7 @@ export class CmakeAdapter implements TestAdapter { message: e.toString(), }); } finally { - this.currentTest = undefined; + this.currentTestProcess = undefined; } } @@ -312,28 +335,72 @@ export class CmakeAdapter implements TestAdapter { // Single test // - const test = this.cmakeTests.find(test => test.name === id); + const test = this.cmakeTests.find((test) => test.name === id); if (!test) { - // Not found + // Not found, mark test as skipped. + this.testStatesEmitter.fire({ + type: 'test', + test: id, + state: 'skipped', + }); return; } // Debug test - // TODO allow custom configs - const defaultConfig: vscode.DebugConfiguration = { - name: `CTest ${test.name}`, - type: 'cppdbg', - request: 'launch', - windows: { - type: 'cppvsdbg', - }, - }; - const config = { - ...defaultConfig, - ...getCmakeTestDebugConfiguration(test), - }; - // TODO monitor sessions? Is it useful? see onDidStartDebugSession/onDidTerminateDebugSession - console.log(config); - await vscode.debug.startDebugging(this.workspaceFolder, config); + this.testStatesEmitter.fire({ + type: 'test', + test: id, + state: 'running', + }); + try { + // Get test config + const config = vscode.workspace.getConfiguration( + 'cmakeExplorer', + this.workspaceFolder.uri + ); + const debugConfig = config.get('debugConfig'); + const defaultConfig: vscode.DebugConfiguration = { + name: 'CTest', + type: 'cppdbg', + request: 'launch', + windows: { + type: 'cppvsdbg', + }, + linux: { + type: 'cppdbg', + MIMode: 'gdb', + }, + osx: { + type: 'cppdbg', + MIMode: 'lldb', + }, + }; + + // Remember test-specific config for the DebugConfigurationProvider registered + // in the constructor (method resolveDebugConfiguration) + this.debuggedTestConfig = getCmakeTestDebugConfiguration(test); + + // Start the debugging session. The actual debug config will combine the + // global and test-specific values + await vscode.debug.startDebugging( + this.workspaceFolder, + debugConfig || defaultConfig + ); + // TODO monitor sessions? Is it useful? see onDidStartDebugSession/onDidTerminateDebugSession + this.testStatesEmitter.fire({ + type: 'test', + test: id, + state: 'passed', + }); + } catch (e) { + this.testStatesEmitter.fire({ + type: 'test', + test: id, + state: 'errored', + message: e.toString(), + }); + } finally { + this.debuggedTestConfig = undefined; + } } } diff --git a/src/cmake-runner.ts b/src/cmake-runner.ts index fc7e9b8..148a189 100644 --- a/src/cmake-runner.ts +++ b/src/cmake-runner.ts @@ -71,7 +71,7 @@ export function loadCmakeTests( // Capture result on stdout const out: string[] = []; - ctestProcess.stdout.on('data', data => { + ctestProcess.stdout.on('data', (data) => { out.push(data); }); @@ -113,7 +113,7 @@ export function scheduleCmakeTest( const { name, config } = test; const WORKING_DIRECTORY = test.properties.find( - p => p.name === 'WORKING_DIRECTORY' + (p) => p.name === 'WORKING_DIRECTORY' ); const cwd = WORKING_DIRECTORY ? WORKING_DIRECTORY.value : undefined; const testProcess = child_process.spawn( @@ -147,13 +147,13 @@ export function executeCmakeTest( try { // Capture result on stdout const out: string[] = []; - testProcess.stdout.on('data', data => { + testProcess.stdout.on('data', (data) => { out.push(data); }); // The 'exit' event is always sent even if the child process crashes or is // killed so we can safely resolve/reject the promise from there - testProcess.once('exit', code => { + testProcess.once('exit', (code) => { const result: CmakeTestResult = { code, out: out.length ? out.join('') : undefined, @@ -201,10 +201,11 @@ export function getCmakeTestDebugConfiguration( ): Partial { const [command, ...args] = test.command; const WORKING_DIRECTORY = test.properties.find( - p => p.name === 'WORKING_DIRECTORY' + (p) => p.name === 'WORKING_DIRECTORY' ); const cwd = WORKING_DIRECTORY ? WORKING_DIRECTORY.value : undefined; return { + name: `CTest ${test.name}`, program: command, args, cwd, @@ -226,10 +227,7 @@ export function getCtestPath(cwd: string) { } // Extract CTest path from cache file. - const match = fs - .readFileSync(cacheFilePath) - .toString() - .match(CTEST_RE); + const match = fs.readFileSync(cacheFilePath).toString().match(CTEST_RE); if (!match) { throw new Error( `CTest path not found in CMake cache file ${cacheFilePath}` diff --git a/src/main.ts b/src/main.ts index 29ed097..c29b493 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,7 +34,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( new TestAdapterRegistrar( testHub, - workspaceFolder => new CmakeAdapter(workspaceFolder, log), + (workspaceFolder) => new CmakeAdapter(workspaceFolder, log, context), log ) );