Skip to content

Commit

Permalink
Add custom debug configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
fredericbonnet committed Jun 29, 2020
1 parent 77ccfc8 commit 8dfd623
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 35 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
},
"devDependencies": {
"@types/vscode": "~1.23.0",
"prettier": "^2.0.5",
"typescript": "^3.5.3",
"vsce": "^1.65.0"
},
Expand Down Expand Up @@ -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",
Expand Down
117 changes: 92 additions & 25 deletions src/cmake-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.DebugConfiguration>;

//
// TestAdapter implementations
Expand Down Expand Up @@ -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<vscode.DebugConfiguration> => {
return {
...config,
...this.debuggedTestConfig,
};
},
})
);

this.disposables.push(this.testsEmitter);
this.disposables.push(this.testStatesEmitter);
this.disposables.push(this.autorunEmitter);
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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(<TestEvent>{
Expand All @@ -273,12 +294,14 @@ export class CmakeAdapter implements TestAdapter {
this.workspaceFolder.uri
);
const extraCtestRunArgs = config.get<string>('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(<TestEvent>{
type: 'test',
test: id,
Expand All @@ -293,7 +316,7 @@ export class CmakeAdapter implements TestAdapter {
message: e.toString(),
});
} finally {
this.currentTest = undefined;
this.currentTestProcess = undefined;
}
}

Expand All @@ -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(<TestEvent>{
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(<TestEvent>{
type: 'test',
test: id,
state: 'running',
});
try {
// Get test config
const config = vscode.workspace.getConfiguration(
'cmakeExplorer',
this.workspaceFolder.uri
);
const debugConfig = config.get<string>('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(<TestEvent>{
type: 'test',
test: id,
state: 'passed',
});
} catch (e) {
this.testStatesEmitter.fire(<TestEvent>{
type: 'test',
test: id,
state: 'errored',
message: e.toString(),
});
} finally {
this.debuggedTestConfig = undefined;
}
}
}
16 changes: 7 additions & 9 deletions src/cmake-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -201,10 +201,11 @@ export function getCmakeTestDebugConfiguration(
): Partial<vscode.DebugConfiguration> {
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,
Expand All @@ -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}`
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
);
Expand Down

0 comments on commit 8dfd623

Please sign in to comment.