-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add multi-pass environment variable resolution #2322
Merged
bobbrow
merged 11 commits into
microsoft:master
from
john-patterson:feat/multi-pass-env
Jul 24, 2018
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5e79538
fix unit test launch.json target
5ff8006
fix res. of vars with env,config,workspaceFolder
8736bb8
tests existing tag resolution functionality
john-patterson 419afd0
rough draft
john-patterson 012e2df
refactors common test file
john-patterson 91e4dcf
fixes linting errors in common.ts
john-patterson 06ad729
refactors resolveVariables implementation
6568cef
adds specific test for user scenario
d220d5d
fix cycle detection and half open detection
735c1f3
simplifies parsing into single pass
john-patterson ebf9a0b
fixes Travis CI failing unitTests
john-patterson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -178,54 +178,59 @@ export function resolveVariables(input: string, additionalEnvironment: {[key: st | |
} | ||
|
||
// Replace environment and configuration variables. | ||
let regexp: RegExp = /\$\{((env|config|workspaceFolder)(.|:))?(.*?)\}/g; | ||
let ret: string = input.replace(regexp, (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { | ||
// Historically, if the variable didn't have anything before the "." or ":" | ||
// it was assumed to be an environment variable | ||
if (varType === undefined) { | ||
varType = "env"; | ||
} | ||
let newValue: string = undefined; | ||
switch (varType) { | ||
case "env": { | ||
let v: string | string[] = additionalEnvironment[name]; | ||
if (typeof v === "string") { | ||
newValue = v; | ||
} else if (input === match && v instanceof Array) { | ||
newValue = v.join(";"); | ||
} | ||
if (!newValue) { | ||
newValue = process.env[name]; | ||
} | ||
break; | ||
let regexp: () => RegExp = () => /\$\{((env|config|workspaceFolder)(\.|:))?(.*?)\}/g; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch 👍 |
||
let ret: string = input; | ||
let cycleCache: Set<string> = new Set(); | ||
while (!cycleCache.has(ret)) { | ||
cycleCache.add(ret); | ||
ret = ret.replace(regexp(), (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { | ||
// Historically, if the variable didn't have anything before the "." or ":" | ||
// it was assumed to be an environment variable | ||
if (varType === undefined) { | ||
varType = "env"; | ||
} | ||
case "config": { | ||
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); | ||
if (config) { | ||
newValue = config.get<string>(name); | ||
let newValue: string = undefined; | ||
switch (varType) { | ||
case "env": { | ||
let v: string | string[] = additionalEnvironment[name]; | ||
if (typeof v === "string") { | ||
newValue = v; | ||
} else if (input === match && v instanceof Array) { | ||
newValue = v.join(";"); | ||
} | ||
if (!newValue) { | ||
newValue = process.env[name]; | ||
} | ||
break; | ||
} | ||
break; | ||
} | ||
case "workspaceFolder": { | ||
// Only replace ${workspaceFolder:name} variables for now. | ||
// We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also | ||
// intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) | ||
if (name && vscode.workspace && vscode.workspace.workspaceFolders) { | ||
let folder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); | ||
if (folder) { | ||
newValue = folder.uri.fsPath; | ||
case "config": { | ||
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); | ||
if (config) { | ||
newValue = config.get<string>(name); | ||
} | ||
break; | ||
} | ||
case "workspaceFolder": { | ||
// Only replace ${workspaceFolder:name} variables for now. | ||
// We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also | ||
// intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) | ||
if (name && vscode.workspace && vscode.workspace.workspaceFolders) { | ||
let folder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); | ||
if (folder) { | ||
newValue = folder.uri.fsPath; | ||
} | ||
} | ||
break; | ||
} | ||
break; | ||
default: { assert.fail("unknown varType matched"); } | ||
} | ||
default: { assert.fail("unknown varType matched"); } | ||
} | ||
return (newValue) ? newValue : match; | ||
}); | ||
return (newValue) ? newValue : match; | ||
}); | ||
} | ||
|
||
// Resolve '~' at the start of the path. | ||
regexp = /^\~/g; | ||
ret = ret.replace(regexp, (match: string, name: string) => { | ||
regexp = () => /^\~/g; | ||
ret = ret.replace(regexp(), (match: string, name: string) => { | ||
let newValue: string = process.env.HOME; | ||
return (newValue) ? newValue : match; | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
/* -------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All Rights Reserved. | ||
* See 'LICENSE' in the project root for license information. | ||
* ------------------------------------------------------------------------------------------ */ | ||
|
||
import * as assert from "assert"; | ||
import { resolveVariables } from "../../src/common"; | ||
|
||
suite("Common Utility validation", () => { | ||
suite("resolveVariables", () => { | ||
const success: string = "success"; | ||
const home: string = process.env.HOME; | ||
|
||
test("raw input", () => { | ||
const input: string = "test"; | ||
inputAndEnvironment(input, {}) | ||
.shouldResolveTo(input); | ||
}); | ||
|
||
test("raw input with tilde", () => { | ||
inputAndEnvironment("~/test", {}) | ||
.shouldResolveTo(`${home}/test`); | ||
}); | ||
|
||
test("env input with tilde", () => { | ||
inputAndEnvironment("${path}/test", { | ||
path: home | ||
}) | ||
.shouldResolveTo(`${home}/test`); | ||
}); | ||
|
||
test("solo env input resulting in array", () => { | ||
inputAndEnvironment("${test}", { | ||
test: ["foo", "bar"] | ||
}) | ||
.shouldResolveTo("foo;bar"); | ||
}); | ||
|
||
test("mixed raw and env input resulting in array", () => { | ||
const input: string = "baz${test}"; | ||
resolveVariablesWithInput(input) | ||
.withEnvironment({ | ||
test: ["foo", "bar"] | ||
}) | ||
.shouldResolveTo(input); | ||
}); | ||
|
||
test("solo env input not in env config finds process env", () => { | ||
const processKey: string = `cpptoolstests_${Date.now()}`; | ||
const input: string = "foo${" + processKey + "}"; | ||
let actual: string; | ||
try { | ||
process.env[processKey] = "bar"; | ||
actual = resolveVariables(input, {}); | ||
} finally { | ||
delete process.env[processKey]; | ||
} | ||
assert.equal(actual, "foobar"); | ||
}); | ||
|
||
test("env input", () => { | ||
resolveVariablesWithInput("${test}") | ||
.withEnvironment({ | ||
"test": success | ||
}) | ||
.shouldResolveTo(success); | ||
}); | ||
|
||
test("env input mixed with plain text", () => { | ||
resolveVariablesWithInput("${test}bar") | ||
.withEnvironment({ | ||
"test": "foo" | ||
}) | ||
.shouldResolveTo("foobar"); | ||
}); | ||
|
||
test("env input with two variables", () => { | ||
resolveVariablesWithInput("f${a}${b}r") | ||
.withEnvironment({ | ||
a: "oo", | ||
b: "ba" | ||
}) | ||
.shouldResolveTo("foobar"); | ||
}); | ||
|
||
test("env input not in env", () => { | ||
const input: string = "${test}"; | ||
resolveVariablesWithInput(input) | ||
.withEnvironment({}) | ||
.shouldResolveTo(input); | ||
}); | ||
|
||
test("env with macro inside environment definition", () => { | ||
resolveVariablesWithInput("${arm6.include}") | ||
.withEnvironment({ | ||
"envRoot": "apps/tool/buildenv", | ||
"arm6.include": "${envRoot}/arm6/include" | ||
}) | ||
.shouldResolveTo("apps/tool/buildenv/arm6/include"); | ||
}); | ||
|
||
test("env nested with half open variable", () => { | ||
resolveVariablesWithInput("${arm6.include}") | ||
.withEnvironment({ | ||
"envRoot": "apps/tool/buildenv", | ||
"arm6.include": "${envRoot/arm6/include" | ||
}) | ||
.shouldResolveTo("${envRoot/arm6/include"); | ||
}); | ||
|
||
test("env nested with half closed variable", () => { | ||
resolveVariablesWithInput("${arm6.include}") | ||
.withEnvironment({ | ||
"envRoot": "apps/tool/buildenv", | ||
"arm6.include": "envRoot}/arm6/include" | ||
}) | ||
.shouldResolveTo("envRoot}/arm6/include"); | ||
}); | ||
|
||
test("env nested with a cycle", () => { | ||
resolveVariablesWithInput("${a}") | ||
.withEnvironment({ | ||
"a": "${b}", | ||
"b": "${c}", | ||
"c": "${a}" | ||
}) | ||
.shouldResolveTo("${a}"); | ||
}); | ||
|
||
test("env input with 1 level of nested variables anchored at end", () => { | ||
resolveVariablesWithInput("${foo${test}}") | ||
.withEnvironment({ | ||
"foobar": success, | ||
"test": "bar" | ||
}) | ||
.shouldResolveTo("${foo${test}}"); | ||
}); | ||
|
||
test("env input with 1 level of nested variables anchored in the middle", () => { | ||
resolveVariablesWithInput("${f${test}r}") | ||
.withEnvironment({ | ||
"foobar": success, | ||
"test": "ooba" | ||
}) | ||
.shouldResolveTo("${f${test}r}"); | ||
}); | ||
|
||
test("env input with 1 level of nested variable anchored at front", () => { | ||
resolveVariablesWithInput("${${test}bar}") | ||
.withEnvironment({ | ||
"foobar": success, | ||
"test": "foo" | ||
}) | ||
.shouldResolveTo("${${test}bar}"); | ||
}); | ||
|
||
test("env input with 3 levels of nested variables", () => { | ||
resolveVariablesWithInput("${foo${a${b${c}}}}") | ||
.withEnvironment({ | ||
"foobar": success, | ||
"a1": "bar", | ||
"b2": "1", | ||
"c": "2" | ||
}) | ||
.shouldResolveTo("${foo${a${b${c}}}}"); | ||
}); | ||
|
||
test("env input contains env", () => { | ||
resolveVariablesWithInput("${envRoot}") | ||
.shouldLookupSymbol("envRoot"); | ||
}); | ||
|
||
test("env input contains config", () => { | ||
resolveVariablesWithInput("${configRoot}") | ||
.shouldLookupSymbol("configRoot"); | ||
}); | ||
|
||
test("env input contains workspaceFolder", () => { | ||
resolveVariablesWithInput("${workspaceFolderRoot}") | ||
.shouldLookupSymbol("workspaceFolderRoot"); | ||
}); | ||
|
||
test("input contains env.", () => { | ||
resolveVariablesWithInput("${env.Root}") | ||
.shouldLookupSymbol("Root"); | ||
}); | ||
|
||
test("input contains env:", () => { | ||
resolveVariablesWithInput("${env:Root}") | ||
.shouldLookupSymbol("Root"); | ||
}); | ||
|
||
interface ResolveTestFlowEnvironment { | ||
withEnvironment(additionalEnvironment: {[key: string]: string | string[]}): ResolveTestFlowAssert; | ||
shouldLookupSymbol: (key: string) => void; | ||
} | ||
interface ResolveTestFlowAssert { | ||
shouldResolveTo: (x: string) => void; | ||
} | ||
|
||
function resolveVariablesWithInput(input: string): ResolveTestFlowEnvironment { | ||
return { | ||
withEnvironment: (additionalEnvironment: {[key: string]: string | string[]}) => { | ||
return inputAndEnvironment(input, additionalEnvironment); | ||
}, | ||
shouldLookupSymbol: (symbol: string) => { | ||
const environment: {[key: string]: string | string[]} = {}; | ||
environment[symbol] = success; | ||
return inputAndEnvironment(input, environment) | ||
.shouldResolveTo(success); | ||
} | ||
}; | ||
} | ||
|
||
function inputAndEnvironment(input: string, additionalEnvironment: {[key: string]: string | string[]}): ResolveTestFlowAssert { | ||
return { | ||
shouldResolveTo: (expected: string) => { | ||
const actual: string = resolveVariables(input, additionalEnvironment); | ||
const msg: string = `Expected ${expected}. Got ${actual} with input ${input} and environment ${JSON.stringify(additionalEnvironment)}.`; | ||
assert.equal(actual, expected, msg); | ||
} | ||
}; | ||
} | ||
|
||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's interesting...I guess we don't Launch Tests very much :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good so far. I'm not done reviewing yet...