Skip to content

Commit

Permalink
Add support for extra CTest environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
fredericbonnet committed Nov 18, 2021
1 parent b0a3a0f commit 5ac639c
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 26 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 support for extra CTest environment variables. This addresses issue #33.

## [0.14.2] - 2021-11-11

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -50,6 +51,9 @@ be substituted with the home path on Unix systems.
| ------------------ | ----------------------------------------------------------------- |
| `${env:<VARNAME>}` | 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
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
172 changes: 148 additions & 24 deletions src/cmake-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<vscode.DebugConfiguration> => {
return {
...config,
...debuggedTestConfig,
};
},
): vscode.ProviderResult<vscode.DebugConfiguration> =>
mergeConfigs(config),
})
);

Expand Down Expand Up @@ -540,17 +557,30 @@ export class CmakeAdapter implements TestAdapter {
key: string
) {
const configStr = config.get<string>(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
Expand All @@ -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;
Expand Down Expand Up @@ -633,23 +670,110 @@ 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
*
* @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<string, string>) => {
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;
};
10 changes: 8 additions & 2 deletions src/cmake-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -166,6 +169,7 @@ export function scheduleCmakeTestProcess(
{
ctestPath,
cwd,
env,
parallelJobs,
buildConfig,
extraArgs,
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 5ac639c

Please sign in to comment.