From 5ac639cd7066336172ff2dd6a584e238f5ede469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Bonnet?= Date: Thu, 18 Nov 2021 22:58:56 +0100 Subject: [PATCH] Add support for extra CTest environment variables --- CHANGELOG.md | 4 + README.md | 4 + package.json | 9 +++ src/cmake-adapter.ts | 172 +++++++++++++++++++++++++++++++++++++------ src/cmake-runner.ts | 10 ++- 5 files changed, 173 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0623b..720d983 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 support for extra CTest environment variables. This addresses issue #33. + ## [0.14.2] - 2021-11-11 ### Fixed diff --git a/README.md b/README.md index cbdc03d..cc0324f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explor | `cmakeExplorer.parallelJobs` | Maximum number of parallel test jobs to run (zero=autodetect, 1 or negative=disable). See [Parallel test jobs](#parallel-test-jobs) for more info. | 0 | | `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`. | Empty | | `cmakeExplorer.extraCtestRunArgs` | Extra command-line arguments passed to CTest at run time. For example, `-V` will enable verbose output from tests. | Empty | +| `cmakeExplorer.extraCtestEnvVars` | Extra environment variables passed to CTest at run time. | Empty | | `cmakeExplorer.suiteDelimiter` | Delimiter used to split CMake test names into suite/test hierarchy. For example, if you name your tests `suite1/subsuite1/test1`, `suite1/subsuite1/test2`, `suite2/subsuite3/test4`, etc. you may set this to `/` in order to group your suites into a tree. If empty, the tests are not grouped. | Empty | | `cmakeExplorer.testFileVar` | CTest environment variable defined for a test, giving the path of the source file containing the test. See [Source files](#source-files) for more info. | Empty | | `cmakeExplorer.testLineVar` | CTest environment variable defined for a test, giving the line number within the file where the test definition starts (if known). See [Source files](#source-files) for more info. | Empty | @@ -50,6 +51,9 @@ be substituted with the home path on Unix systems. | ------------------ | ----------------------------------------------------------------- | | `${env:}` | The value of the environment variable `VARNAME` at session start. | +_(Note: On Windows, variable names are case insensitive but must be uppercase +for `env:` substitition to work properly)_ + Additionally, if the [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools) extension is active in the current workspace and diff --git a/package.json b/package.json index 71913ed..14f17d6 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,15 @@ "default": "", "scope": "resource" }, + "cmakeExplorer.extraCtestEnvVars": { + "description": "Extra environment variables passed to CTest at run time", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "scope": "resource" + }, "cmakeExplorer.testFileVar": { "description": "CTest environment variable defined for a test, giving the path of the source file containing the test", "type": "string", diff --git a/src/cmake-adapter.ts b/src/cmake-adapter.ts index d38fed3..d0241da 100644 --- a/src/cmake-adapter.ts +++ b/src/cmake-adapter.ts @@ -366,11 +366,13 @@ export class CmakeAdapter implements TestAdapter { 'buildConfig', 'extraCtestRunArgs', ]); + const extraCtestEnvVars = await this.getConfigObject('extraCtestEnvVars'); const parallelJobs = this.getParallelJobs(); return { ctestPath: this.ctestPath, cwd: path.resolve(this.workspaceFolder.uri.fsPath, buildDir), + env: mergeVariablesIntoProcessEnv(extraCtestEnvVars), parallelJobs, buildConfig, extraArgs: extraCtestRunArgs, @@ -437,27 +439,42 @@ export class CmakeAdapter implements TestAdapter { this.log.info(`Debugging CMake test ${id}`); const disposables: vscode.Disposable[] = []; try { + // Get & substitute config settings + const extraCtestEnvVars = await this.getConfigObject('extraCtestEnvVars'); + // Get global debug config const [debugConfig] = await this.getConfigStrings(['debugConfig']); const defaultConfig = this.getDefaultDebugConfiguration(); // Get test-specific debug config - const debuggedTestConfig = getCmakeTestDebugConfiguration(test); + const { env, ...debuggedTestConfig } = + getCmakeTestDebugConfiguration(test); + + // Utilities to merge configs and environment variables + const mergeEnvironments = (environment: DebugEnvironment) => + mergeVariablesIntoDebugEnv( + mergeVariablesIntoDebugEnv(environment, extraCtestEnvVars), + env + ); + const mergeConfigs = ({ + environment, + ...config + }: vscode.DebugConfiguration) => ({ + ...config, + ...debuggedTestConfig, + environment: mergeEnvironments(environment), + }); // Register a DebugConfigurationProvider to combine global and // test-specific debug configurations before the debugging session starts disposables.push( vscode.debug.registerDebugConfigurationProvider('*', { - resolveDebugConfiguration: ( + resolveDebugConfigurationWithSubstitutedVariables: ( folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken - ): vscode.ProviderResult => { - return { - ...config, - ...debuggedTestConfig, - }; - }, + ): vscode.ProviderResult => + mergeConfigs(config), }) ); @@ -540,17 +557,30 @@ export class CmakeAdapter implements TestAdapter { key: string ) { const configStr = config.get(key) || ''; - let str = configStr; - varMap.forEach((value, key) => { - while (str.indexOf(key) > -1) { - str = str.replace(key, value); - } - }); - return str; + return substituteString(configStr, varMap); + } + + /** + * Get & substitute config object + * + * @param name Config object name + * + * @return Config object values + */ + private async getConfigObject(name: string) { + const config = this.getWorkspaceConfiguration(); + const varMap = await this.getVariableSubstitutionMap(); + const obj = config.get<{ [key: string]: string }>(name) || {}; + for (let key in obj) { + obj[key] = substituteString(obj[key], varMap); + } + return obj; } /** * Get variable to value substitution map for config strings + * + * @note on Windows environment variable names are converted to uppercase */ private async getVariableSubstitutionMap() { // Standard variables @@ -572,7 +602,14 @@ export class CmakeAdapter implements TestAdapter { // Environment variables prefixed by 'env:' for (const [varname, value] of Object.entries(process.env)) { - if (value !== undefined) substitutionMap.set(`\${env:${varname}}`, value); + if (value !== undefined) { + substitutionMap.set( + `\${env:${ + process.platform == 'win32' ? varname.toUpperCase() : varname + }}`, + value + ); + } } return substitutionMap; @@ -633,10 +670,8 @@ const getTestFileInfo = ( * @param env Map of environment variables * @param varname Variable name to get value for */ -const getFileFromEnvironment = ( - env: { [key: string]: string }, - fileVar: string -) => env[fileVar]; +const getFileFromEnvironment = (env: NodeJS.ProcessEnv, fileVar: string) => + env[fileVar]; /** * Get line number from environment variables @@ -644,12 +679,101 @@ const getFileFromEnvironment = ( * @param env Map of environment variables * @param varname Variable name to get value for */ -const getLineFromEnvironment = ( - env: { [key: string]: string }, - varname: string -) => { +const getLineFromEnvironment = (env: NodeJS.ProcessEnv, varname: string) => { const value = env[varname]; // Test Explorer expects 0-indexed line numbers if (value) return Number.parseInt(value) - 1; return; }; + +/** + * Substitute variables in string + * + * @param str String to substitute + * @param varMap Variable to value map + * + * @return Substituted string + */ +const substituteString = (str: string, varMap: Map) => { + varMap.forEach((value, key) => { + while (str.indexOf(key) > -1) { + str = str.replace(key, value); + } + }); + return str; +}; + +/** Debug environment array */ +type DebugEnvironment = { name: string; value: string | undefined }[]; + +/** + * Get key of variable in process environment + * + * Some platforms such as Win32 have case-insensitive environment variables + * + * @param varname Variable name + * @param env Process environment + */ +const getVariableKey = (varname: string, env: NodeJS.ProcessEnv) => + process.platform === 'win32' + ? Object.keys(env).find( + (key) => key.toUpperCase() == varname.toUpperCase() + ) || varname + : varname; + +/** + * Get index of variable in debug environment + * + * Some platforms such as Win32 have case-insensitive environment variables + * + * @param varname Variable name + * @param environment Debug environment + */ +const getVariableIndex = (varname: string, environment: DebugEnvironment) => + process.platform === 'win32' + ? environment.findIndex( + ({ name }) => name.toUpperCase() == varname.toUpperCase() + ) + : environment.findIndex(({ name }) => name == varname); + +/** + * Merge variables into process environment + * + * @param variables Variables to merge + * + * @return Environment with variables merged + */ +const mergeVariablesIntoProcessEnv = (variables: { + [name: string]: string; +}) => { + const result = { ...process.env }; + for (let name in variables) { + delete result[getVariableKey(name, process.env)]; + result[name] = variables[name]; + } + return result; +}; + +/** + * Merge variables into debug environment + * + * @param environment Target environment + * @param variables Variables to merge + * + * @return Environment with variables merged + */ +const mergeVariablesIntoDebugEnv = ( + environment: DebugEnvironment, + variables: { [name: string]: string } +) => { + const result = [...environment]; + for (let name in variables) { + const variableIndex = getVariableIndex(name, environment); + if (variableIndex == -1) { + result.push({ name, value: variables[name] }); + } else { + result[variableIndex] = { name, value: variables[name] }; + } + } + return result; +}; diff --git a/src/cmake-runner.ts b/src/cmake-runner.ts index de46701..05b8574 100644 --- a/src/cmake-runner.ts +++ b/src/cmake-runner.ts @@ -145,6 +145,9 @@ export type CmakeTestRunOptions = { /** CMake build directory to run the command within */ cwd: string; + /** Environment */ + env: NodeJS.ProcessEnv; + /** Number of jobs to run in parallel */ parallelJobs: number; @@ -166,6 +169,7 @@ export function scheduleCmakeTestProcess( { ctestPath, cwd, + env, parallelJobs, buildConfig, extraArgs, @@ -190,7 +194,7 @@ export function scheduleCmakeTestProcess( ...testList, ...args, ], - { cwd } + { cwd, env } ); if (!testProcess.pid) { // Something failed, e.g. the executable or cwd doesn't exist @@ -313,7 +317,9 @@ export function getCtestPath(cwd: string) { * * @param test CMake test info */ -export function getCmakeTestEnvironmentVariables(test: CmakeTestInfo) { +export function getCmakeTestEnvironmentVariables( + test: CmakeTestInfo +): NodeJS.ProcessEnv | undefined { const ENVIRONMENT = test.properties.find((p) => p.name === 'ENVIRONMENT'); if (!ENVIRONMENT) return;