Path: blob/main/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts
3297 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { stub } from 'sinon';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';9import { Schemas } from '../../../../../base/common/network.js';10import { IPath, normalize } from '../../../../../base/common/path.js';11import * as platform from '../../../../../base/common/platform.js';12import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';13import { isObject } from '../../../../../base/common/types.js';14import { URI } from '../../../../../base/common/uri.js';15import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';16import { Selection } from '../../../../../editor/common/core/selection.js';17import { EditorType } from '../../../../../editor/common/editorCommon.js';18import { ICommandService } from '../../../../../platform/commands/common/commands.js';19import { IConfigurationOverrides, IConfigurationService, IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js';20import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';21import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';22import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter, Verbosity } from '../../../../../platform/label/common/label.js';23import { IWorkspace, IWorkspaceFolder, IWorkspaceIdentifier, Workspace } from '../../../../../platform/workspace/common/workspace.js';24import { testWorkspace } from '../../../../../platform/workspace/test/common/testWorkspace.js';25import { TestEditorService, TestQuickInputService } from '../../../../test/browser/workbenchTestServices.js';26import { TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';27import { IExtensionService } from '../../../extensions/common/extensions.js';28import { IPathService } from '../../../path/common/pathService.js';29import { BaseConfigurationResolverService } from '../../browser/baseConfigurationResolverService.js';30import { IConfigurationResolverService } from '../../common/configurationResolver.js';31import { ConfigurationResolverExpression } from '../../common/configurationResolverExpression.js';3233const mockLineNumber = 10;34class TestEditorServiceWithActiveEditor extends TestEditorService {35override get activeTextEditorControl(): any {36return {37getEditorType() {38return EditorType.ICodeEditor;39},40getSelection() {41return new Selection(mockLineNumber, 1, mockLineNumber, 10);42}43};44}45override get activeEditor(): any {46return {47get resource(): any {48return URI.parse('file:///VSCode/workspaceLocation/file');49}50};51}52}5354class TestConfigurationResolverService extends BaseConfigurationResolverService {5556}5758const nullContext = {59getAppRoot: () => undefined,60getExecPath: () => undefined61};6263suite('Configuration Resolver Service', () => {64let configurationResolverService: IConfigurationResolverService | null;65const envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' };66// let environmentService: MockWorkbenchEnvironmentService;67let mockCommandService: MockCommandService;68let editorService: TestEditorServiceWithActiveEditor;69let containingWorkspace: Workspace;70let workspace: IWorkspaceFolder;71let quickInputService: TestQuickInputService;72let labelService: MockLabelService;73let pathService: MockPathService;74let extensionService: IExtensionService;7576const disposables = ensureNoDisposablesAreLeakedInTestSuite();7778setup(() => {79mockCommandService = new MockCommandService();80editorService = disposables.add(new TestEditorServiceWithActiveEditor());81quickInputService = new TestQuickInputService();82// environmentService = new MockWorkbenchEnvironmentService(envVariables);83labelService = new MockLabelService();84pathService = new MockPathService();85extensionService = new TestExtensionService();86containingWorkspace = testWorkspace(URI.parse('file:///VSCode/workspaceLocation'));87workspace = containingWorkspace.folders[0];88configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));89});9091teardown(() => {92configurationResolverService = null;93});9495test('substitute one', async () => {96if (platform.isWindows) {97assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder} xyz'), 'abc \\VSCode\\workspaceLocation xyz');98} else {99assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder} xyz'), 'abc /VSCode/workspaceLocation xyz');100}101});102103test('does not preserve platform config even when not matched', async () => {104const obj = {105program: 'osx.sh',106windows: {107program: 'windows.exe'108},109linux: {110program: 'linux.sh'111}112};113const config: any = await configurationResolverService!.resolveAsync(workspace, obj);114115const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined;116117assert.strictEqual(config.windows, undefined);118assert.strictEqual(config.osx, undefined);119assert.strictEqual(config.linux, undefined);120assert.strictEqual(config.program, expected);121});122123test('apples platform specific config', async () => {124const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined;125const obj = {126windows: {127program: 'windows.exe'128},129osx: {130program: 'osx.sh'131},132linux: {133program: 'linux.sh'134}135};136const originalObj = JSON.stringify(obj);137const config: any = await configurationResolverService!.resolveAsync(workspace, obj);138139assert.strictEqual(config.program, expected);140assert.strictEqual(config.windows, undefined);141assert.strictEqual(config.osx, undefined);142assert.strictEqual(config.linux, undefined);143assert.strictEqual(JSON.stringify(obj), originalObj); // did not mutate original144});145146test('workspace folder with argument', async () => {147if (platform.isWindows) {148assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc \\VSCode\\workspaceLocation xyz');149} else {150assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc /VSCode/workspaceLocation xyz');151}152});153154test('workspace folder with invalid argument', async () => {155await assert.rejects(async () => await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder:invalidLocation} xyz'));156});157158test('workspace folder with undefined workspace folder', async () => {159await assert.rejects(async () => await configurationResolverService!.resolveAsync(undefined, 'abc ${workspaceFolder} xyz'));160});161162test('workspace folder with argument and undefined workspace folder', async () => {163if (platform.isWindows) {164assert.strictEqual(await configurationResolverService!.resolveAsync(undefined, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc \\VSCode\\workspaceLocation xyz');165} else {166assert.strictEqual(await configurationResolverService!.resolveAsync(undefined, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc /VSCode/workspaceLocation xyz');167}168});169170test('workspace folder with invalid argument and undefined workspace folder', () => {171assert.rejects(async () => await configurationResolverService!.resolveAsync(undefined, 'abc ${workspaceFolder:invalidLocation} xyz'));172});173174test('workspace root folder name', async () => {175assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz');176});177178test('current selected line number', async () => {179assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${lineNumber} xyz'), `abc ${mockLineNumber} xyz`);180});181182test('relative file', async () => {183assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${relativeFile} xyz'), 'abc file xyz');184});185186test('relative file with argument', async () => {187assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${relativeFile:workspaceLocation} xyz'), 'abc file xyz');188});189190test('relative file with invalid argument', () => {191assert.rejects(async () => await configurationResolverService!.resolveAsync(workspace, 'abc ${relativeFile:invalidLocation} xyz'));192});193194test('relative file with undefined workspace folder', async () => {195if (platform.isWindows) {196assert.strictEqual(await configurationResolverService!.resolveAsync(undefined, 'abc ${relativeFile} xyz'), 'abc \\VSCode\\workspaceLocation\\file xyz');197} else {198assert.strictEqual(await configurationResolverService!.resolveAsync(undefined, 'abc ${relativeFile} xyz'), 'abc /VSCode/workspaceLocation/file xyz');199}200});201202test('relative file with argument and undefined workspace folder', async () => {203assert.strictEqual(await configurationResolverService!.resolveAsync(undefined, 'abc ${relativeFile:workspaceLocation} xyz'), 'abc file xyz');204});205206test('relative file with invalid argument and undefined workspace folder', () => {207assert.rejects(async () => await configurationResolverService!.resolveAsync(undefined, 'abc ${relativeFile:invalidLocation} xyz'));208});209210test('substitute many', async () => {211if (platform.isWindows) {212assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${workspaceFolder} - ${workspaceFolder}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation');213} else {214assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${workspaceFolder} - ${workspaceFolder}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation');215}216});217218test('substitute one env variable', async () => {219if (platform.isWindows) {220assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder} ${env:key1} xyz'), 'abc \\VSCode\\workspaceLocation Value for key1 xyz');221} else {222assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, 'abc ${workspaceFolder} ${env:key1} xyz'), 'abc /VSCode/workspaceLocation Value for key1 xyz');223}224});225226test('substitute many env variable', async () => {227if (platform.isWindows) {228assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2');229} else {230assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation Value for key1 - Value for key2');231}232});233234test('disallows nested keys (#77289)', async () => {235assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} ${env:key1${env:key2}}'), 'Value for key1 ');236});237238test('supports extensionDir', async () => {239const getExtension = stub(extensionService, 'getExtension');240getExtension.withArgs('publisher.extId').returns(Promise.resolve({ extensionLocation: URI.file('/some/path') } as IExtensionDescription));241242assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${extensionInstallFolder:publisher.extId}'), URI.file('/some/path').fsPath);243});244245// test('substitute keys and values in object', () => {246// const myObject = {247// '${workspaceRootFolderName}': '${lineNumber}',248// 'hey ${env:key1} ': '${workspaceRootFolderName}'249// };250// assert.deepStrictEqual(configurationResolverService!.resolveAsync(workspace, myObject), {251// 'workspaceLocation': `${editorService.mockLineNumber}`,252// 'hey Value for key1 ': 'workspaceLocation'253// });254// });255256257test('substitute one env variable using platform case sensitivity', async () => {258if (platform.isWindows) {259assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - Value for key1');260} else {261assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - ');262}263});264265test('substitute one configuration variable', async () => {266const configurationService: IConfigurationService = new TestConfigurationService({267editor: {268fontFamily: 'foo'269},270terminal: {271integrated: {272fontFamily: 'bar'273}274}275});276277const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));278assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');279});280281test('inlines an array (#245718)', async () => {282const configurationService: IConfigurationService = new TestConfigurationService({283editor: {284fontFamily: ['foo', 'bar']285},286});287288const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));289assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo,bar xyz');290});291292test('substitute configuration variable with undefined workspace folder', async () => {293const configurationService: IConfigurationService = new TestConfigurationService({294editor: {295fontFamily: 'foo'296}297});298299const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));300assert.strictEqual(await service.resolveAsync(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');301});302303test('substitute many configuration variables', async () => {304const configurationService = new TestConfigurationService({305editor: {306fontFamily: 'foo'307},308terminal: {309integrated: {310fontFamily: 'bar'311}312}313});314315const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));316assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz');317});318319test('substitute one env variable and a configuration variable', async () => {320const configurationService = new TestConfigurationService({321editor: {322fontFamily: 'foo'323},324terminal: {325integrated: {326fontFamily: 'bar'327}328}329});330331const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));332if (platform.isWindows) {333assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz');334} else {335assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo /VSCode/workspaceLocation Value for key1 xyz');336}337});338339test('recursively resolve variables', async () => {340const configurationService = new TestConfigurationService({341key1: 'key1=${config:key2}',342key2: 'key2=${config:key3}',343key3: 'we did it!',344});345346const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));347assert.strictEqual(await service.resolveAsync(workspace, '${config:key1}'), 'key1=key2=we did it!');348});349350test('substitute many env variable and a configuration variable', async () => {351const configurationService = new TestConfigurationService({352editor: {353fontFamily: 'foo'354},355terminal: {356integrated: {357fontFamily: 'bar'358}359}360});361362const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));363if (platform.isWindows) {364assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2');365} else {366assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar /VSCode/workspaceLocation - /VSCode/workspaceLocation Value for key1 - Value for key2');367}368});369370test('mixed types of configuration variables', async () => {371const configurationService = new TestConfigurationService({372editor: {373fontFamily: 'foo',374lineNumbers: 123,375insertSpaces: false376},377terminal: {378integrated: {379fontFamily: 'bar'380}381},382json: {383schemas: [384{385fileMatch: [386'/myfile',387'/myOtherfile'388],389url: 'schemaURL'390}391]392}393});394395const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));396assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz');397});398399test('uses original variable as fallback', async () => {400const configurationService = new TestConfigurationService({401editor: {}402});403404const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));405assert.strictEqual(await service.resolveAsync(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz');406assert.strictEqual(await service.resolveAsync(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz');407});408409test('configuration variables with invalid accessor', () => {410const configurationService = new TestConfigurationService({411editor: {412fontFamily: 'foo'413}414});415416const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService()));417418assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env} xyz'));419assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env:} xyz'));420assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${config} xyz'));421assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${config:} xyz'));422assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${config:editor} xyz'));423assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${config:editor..fontFamily} xyz'));424assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${config:editor.none.none2} xyz'));425});426427test('a single command variable', () => {428429const configuration = {430'name': 'Attach to Process',431'type': 'node',432'request': 'attach',433'processId': '${command:command1}',434'port': 5858,435'sourceMaps': false,436'outDir': null437};438439return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration).then(result => {440assert.deepStrictEqual({ ...result }, {441'name': 'Attach to Process',442'type': 'node',443'request': 'attach',444'processId': 'command1-result',445'port': 5858,446'sourceMaps': false,447'outDir': null448});449450assert.strictEqual(1, mockCommandService.callCount);451});452});453454test('an old style command variable', () => {455const configuration = {456'name': 'Attach to Process',457'type': 'node',458'request': 'attach',459'processId': '${command:commandVariable1}',460'port': 5858,461'sourceMaps': false,462'outDir': null463};464const commandVariables = Object.create(null);465commandVariables['commandVariable1'] = 'command1';466467return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {468assert.deepStrictEqual({ ...result }, {469'name': 'Attach to Process',470'type': 'node',471'request': 'attach',472'processId': 'command1-result',473'port': 5858,474'sourceMaps': false,475'outDir': null476});477478assert.strictEqual(1, mockCommandService.callCount);479});480});481482test('multiple new and old-style command variables', () => {483484const configuration = {485'name': 'Attach to Process',486'type': 'node',487'request': 'attach',488'processId': '${command:commandVariable1}',489'pid': '${command:command2}',490'sourceMaps': false,491'outDir': 'src/${command:command2}',492'env': {493'processId': '__${command:command2}__',494}495};496const commandVariables = Object.create(null);497commandVariables['commandVariable1'] = 'command1';498499return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {500const expected = {501'name': 'Attach to Process',502'type': 'node',503'request': 'attach',504'processId': 'command1-result',505'pid': 'command2-result',506'sourceMaps': false,507'outDir': 'src/command2-result',508'env': {509'processId': '__command2-result__',510}511};512513assert.deepStrictEqual(Object.keys(result), Object.keys(expected));514Object.keys(result).forEach(property => {515const expectedProperty = (<any>expected)[property];516if (isObject(result[property])) {517assert.deepStrictEqual({ ...result[property] }, expectedProperty);518} else {519assert.deepStrictEqual(result[property], expectedProperty);520}521});522assert.strictEqual(2, mockCommandService.callCount);523});524});525526test('a command variable that relies on resolved env vars', () => {527528const configuration = {529'name': 'Attach to Process',530'type': 'node',531'request': 'attach',532'processId': '${command:commandVariable1}',533'value': '${env:key1}'534};535const commandVariables = Object.create(null);536commandVariables['commandVariable1'] = 'command1';537538return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {539540assert.deepStrictEqual({ ...result }, {541'name': 'Attach to Process',542'type': 'node',543'request': 'attach',544'processId': 'Value for key1',545'value': 'Value for key1'546});547548assert.strictEqual(1, mockCommandService.callCount);549});550});551552test('a single prompt input variable', () => {553554const configuration = {555'name': 'Attach to Process',556'type': 'node',557'request': 'attach',558'processId': '${input:input1}',559'port': 5858,560'sourceMaps': false,561'outDir': null562};563564return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {565566assert.deepStrictEqual({ ...result }, {567'name': 'Attach to Process',568'type': 'node',569'request': 'attach',570'processId': 'resolvedEnterinput1',571'port': 5858,572'sourceMaps': false,573'outDir': null574});575576assert.strictEqual(0, mockCommandService.callCount);577});578});579580test('a single pick input variable', () => {581582const configuration = {583'name': 'Attach to Process',584'type': 'node',585'request': 'attach',586'processId': '${input:input2}',587'port': 5858,588'sourceMaps': false,589'outDir': null590};591592return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {593594assert.deepStrictEqual({ ...result }, {595'name': 'Attach to Process',596'type': 'node',597'request': 'attach',598'processId': 'selectedPick',599'port': 5858,600'sourceMaps': false,601'outDir': null602});603604assert.strictEqual(0, mockCommandService.callCount);605});606});607608test('a single command input variable', () => {609610const configuration = {611'name': 'Attach to Process',612'type': 'node',613'request': 'attach',614'processId': '${input:input4}',615'port': 5858,616'sourceMaps': false,617'outDir': null618};619620return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {621622assert.deepStrictEqual({ ...result }, {623'name': 'Attach to Process',624'type': 'node',625'request': 'attach',626'processId': 'arg for command',627'port': 5858,628'sourceMaps': false,629'outDir': null630});631632assert.strictEqual(1, mockCommandService.callCount);633});634});635636test('several input variables and command', () => {637638const configuration = {639'name': '${input:input3}',640'type': '${command:command1}',641'request': '${input:input1}',642'processId': '${input:input2}',643'command': '${input:input4}',644'port': 5858,645'sourceMaps': false,646'outDir': null647};648649return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {650651assert.deepStrictEqual({ ...result }, {652'name': 'resolvedEnterinput3',653'type': 'command1-result',654'request': 'resolvedEnterinput1',655'processId': 'selectedPick',656'command': 'arg for command',657'port': 5858,658'sourceMaps': false,659'outDir': null660});661662assert.strictEqual(2, mockCommandService.callCount);663});664});665666test('input variable with undefined workspace folder', () => {667668const configuration = {669'name': 'Attach to Process',670'type': 'node',671'request': 'attach',672'processId': '${input:input1}',673'port': 5858,674'sourceMaps': false,675'outDir': null676};677678return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, 'tasks').then(result => {679680assert.deepStrictEqual({ ...result }, {681'name': 'Attach to Process',682'type': 'node',683'request': 'attach',684'processId': 'resolvedEnterinput1',685'port': 5858,686'sourceMaps': false,687'outDir': null688});689690assert.strictEqual(0, mockCommandService.callCount);691});692});693694test('contributed variable', () => {695const buildTask = 'npm: compile';696const variable = 'defaultBuildTask';697const configuration = {698'name': '${' + variable + '}',699};700configurationResolverService!.contributeVariable(variable, async () => { return buildTask; });701return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration).then(result => {702assert.deepStrictEqual({ ...result }, {703'name': `${buildTask}`704});705});706});707708test('resolveWithEnvironment', async () => {709const env = {710'VAR_1': 'VAL_1',711'VAR_2': 'VAL_2'712};713const configuration = 'echo ${env:VAR_1}${env:VAR_2}';714const resolvedResult = await configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration);715assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2');716});717718test('substitution in object key', async () => {719720const configuration = {721'name': 'Test',722'mappings': {723'pos1': 'value1',724'${workspaceFolder}/test1': '${workspaceFolder}/test2',725'pos3': 'value3'726}727};728729return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {730731if (platform.isWindows) {732assert.deepStrictEqual({ ...result }, {733'name': 'Test',734'mappings': {735'pos1': 'value1',736'\\VSCode\\workspaceLocation/test1': '\\VSCode\\workspaceLocation/test2',737'pos3': 'value3'738}739});740} else {741assert.deepStrictEqual({ ...result }, {742'name': 'Test',743'mappings': {744'pos1': 'value1',745'/VSCode/workspaceLocation/test1': '/VSCode/workspaceLocation/test2',746'pos3': 'value3'747}748});749}750751assert.strictEqual(0, mockCommandService.callCount);752});753});754});755756757class MockCommandService implements ICommandService {758759public _serviceBrand: undefined;760public callCount = 0;761762onWillExecuteCommand = () => Disposable.None;763onDidExecuteCommand = () => Disposable.None;764public executeCommand(commandId: string, ...args: any[]): Promise<any> {765this.callCount++;766767let result = `${commandId}-result`;768if (args.length >= 1) {769if (args[0] && args[0].value) {770result = args[0].value;771}772}773774return Promise.resolve(result);775}776}777778class MockLabelService implements ILabelService {779_serviceBrand: undefined;780getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string {781return normalize(resource.fsPath);782}783getUriBasenameLabel(resource: URI): string {784throw new Error('Method not implemented.');785}786getWorkspaceLabel(workspace: URI | IWorkspaceIdentifier | IWorkspace, options?: { verbose: Verbosity }): string {787throw new Error('Method not implemented.');788}789getHostLabel(scheme: string, authority?: string): string {790throw new Error('Method not implemented.');791}792public getHostTooltip(): string | undefined {793throw new Error('Method not implemented.');794}795getSeparator(scheme: string, authority?: string): '/' | '\\' {796throw new Error('Method not implemented.');797}798registerFormatter(formatter: ResourceLabelFormatter): IDisposable {799throw new Error('Method not implemented.');800}801registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable {802throw new Error('Method not implemented.');803}804onDidChangeFormatters: Event<IFormatterChangeEvent> = new Emitter<IFormatterChangeEvent>().event;805}806807class MockPathService implements IPathService {808_serviceBrand: undefined;809get path(): Promise<IPath> {810throw new Error('Property not implemented');811}812defaultUriScheme: string = Schemas.file;813fileURI(path: string): Promise<URI> {814throw new Error('Method not implemented.');815}816userHome(options?: { preferLocal: boolean }): Promise<URI>;817userHome(options: { preferLocal: true }): URI;818userHome(options?: { preferLocal: boolean }): Promise<URI> | URI {819const uri = URI.file('c:\\users\\username');820return options?.preferLocal ? uri : Promise.resolve(uri);821}822hasValidBasename(resource: URI, basename?: string): Promise<boolean>;823hasValidBasename(resource: URI, os: platform.OperatingSystem, basename?: string): boolean;824hasValidBasename(resource: URI, arg2?: string | platform.OperatingSystem, name?: string): boolean | Promise<boolean> {825throw new Error('Method not implemented.');826}827resolvedUserHome: URI | undefined;828}829830class MockInputsConfigurationService extends TestConfigurationService {831public override getValue(arg1?: any, arg2?: any): any {832let configuration;833if (arg1 === 'tasks') {834configuration = {835inputs: [836{837id: 'input1',838type: 'promptString',839description: 'Enterinput1',840default: 'default input1'841},842{843id: 'input2',844type: 'pickString',845description: 'Enterinput1',846default: 'option2',847options: ['option1', 'option2', 'option3']848},849{850id: 'input3',851type: 'promptString',852description: 'Enterinput3',853default: 'default input3',854provide: true,855password: true856},857{858id: 'input4',859type: 'command',860command: 'command1',861args: {862value: 'arg for command'863}864}865]866};867}868return configuration;869}870871public override inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {872return {873value: undefined,874defaultValue: undefined,875userValue: undefined,876overrideIdentifiers: []877};878}879}880881suite('ConfigurationResolverExpression', () => {882ensureNoDisposablesAreLeakedInTestSuite();883884test('parse empty object', () => {885const expr = ConfigurationResolverExpression.parse({});886assert.strictEqual(Array.from(expr.unresolved()).length, 0);887assert.deepStrictEqual(expr.toObject(), {});888});889890test('parse simple string', () => {891const expr = ConfigurationResolverExpression.parse({ value: '${env:HOME}' });892const unresolved = Array.from(expr.unresolved());893assert.strictEqual(unresolved.length, 1);894assert.strictEqual(unresolved[0].name, 'env');895assert.strictEqual(unresolved[0].arg, 'HOME');896});897898test('parse string with argument and colon', () => {899const expr = ConfigurationResolverExpression.parse({ value: '${config:path:to:value}' });900const unresolved = Array.from(expr.unresolved());901assert.strictEqual(unresolved.length, 1);902assert.strictEqual(unresolved[0].name, 'config');903assert.strictEqual(unresolved[0].arg, 'path:to:value');904});905906test('parse object with nested variables', () => {907const expr = ConfigurationResolverExpression.parse({908name: '${env:USERNAME}',909path: '${env:HOME}/folder',910settings: {911value: '${config:path}'912},913array: ['${env:TERM}', { key: '${env:KEY}' }]914});915916const unresolved = Array.from(expr.unresolved());917assert.strictEqual(unresolved.length, 5);918assert.deepStrictEqual(unresolved.map(r => r.name).sort(), ['config', 'env', 'env', 'env', 'env']);919});920921test('resolve and get result', () => {922const expr = ConfigurationResolverExpression.parse({923name: '${env:USERNAME}',924path: '${env:HOME}/folder'925});926927expr.resolve({ inner: 'env:USERNAME', id: '${env:USERNAME}', name: 'env', arg: 'USERNAME' }, 'testuser');928expr.resolve({ inner: 'env:HOME', id: '${env:HOME}', name: 'env', arg: 'HOME' }, '/home/testuser');929930assert.deepStrictEqual(expr.toObject(), {931name: 'testuser',932path: '/home/testuser/folder'933});934});935936test('keeps unresolved variables', () => {937const expr = ConfigurationResolverExpression.parse({938name: '${env:USERNAME}'939});940941assert.deepStrictEqual(expr.toObject(), {942name: '${env:USERNAME}'943});944});945946test('deduplicates identical variables', () => {947const expr = ConfigurationResolverExpression.parse({948first: '${env:HOME}',949second: '${env:HOME}'950});951952const unresolved = Array.from(expr.unresolved());953assert.strictEqual(unresolved.length, 1);954assert.strictEqual(unresolved[0].name, 'env');955assert.strictEqual(unresolved[0].arg, 'HOME');956957expr.resolve(unresolved[0], '/home/user');958assert.deepStrictEqual(expr.toObject(), {959first: '/home/user',960second: '/home/user'961});962});963964test('handles root string value', () => {965const expr = ConfigurationResolverExpression.parse('abc ${env:HOME} xyz');966const unresolved = Array.from(expr.unresolved());967assert.strictEqual(unresolved.length, 1);968assert.strictEqual(unresolved[0].name, 'env');969assert.strictEqual(unresolved[0].arg, 'HOME');970971expr.resolve(unresolved[0], '/home/user');972assert.strictEqual(expr.toObject(), 'abc /home/user xyz');973});974975test('handles root string value with multiple variables', () => {976const expr = ConfigurationResolverExpression.parse('${env:HOME}/folder${env:SHELL}');977const unresolved = Array.from(expr.unresolved());978assert.strictEqual(unresolved.length, 2);979980expr.resolve({ id: '${env:HOME}', inner: 'env:HOME', name: 'env', arg: 'HOME' }, '/home/user');981expr.resolve({ id: '${env:SHELL}', inner: 'env:SHELL', name: 'env', arg: 'SHELL' }, '/bin/bash');982assert.strictEqual(expr.toObject(), '/home/user/folder/bin/bash');983});984985test('handles root string with escaped variables', () => {986const expr = ConfigurationResolverExpression.parse('abc ${env:HOME${env:USER}} xyz');987const unresolved = Array.from(expr.unresolved());988assert.strictEqual(unresolved.length, 1);989assert.strictEqual(unresolved[0].name, 'env');990assert.strictEqual(unresolved[0].arg, 'HOME${env:USER}');991});992993test('resolves nested values', () => {994const expr = ConfigurationResolverExpression.parse({995name: '${env:REDIRECTED}',996'key that is ${env:REDIRECTED}': 'cool!',997});998999for (const r of expr.unresolved()) {1000if (r.arg === 'REDIRECTED') {1001expr.resolve(r, 'username: ${env:USERNAME}');1002} else if (r.arg === 'USERNAME') {1003expr.resolve(r, 'testuser');1004}1005}10061007assert.deepStrictEqual(expr.toObject(), {1008name: 'username: testuser',1009'key that is username: testuser': 'cool!'1010});1011});10121013test('resolves nested values 2 (#245798)', () => {1014const expr = ConfigurationResolverExpression.parse({1015env: {1016SITE: "${input:site}",1017TLD: "${input:tld}",1018HOST: "${input:host}",1019},1020});10211022for (const r of expr.unresolved()) {1023if (r.arg === 'site') {1024expr.resolve(r, 'example');1025} else if (r.arg === 'tld') {1026expr.resolve(r, 'com');1027} else if (r.arg === 'host') {1028expr.resolve(r, 'local.${input:site}.${input:tld}');1029}1030}10311032assert.deepStrictEqual(expr.toObject(), {1033env: {1034SITE: 'example',1035TLD: 'com',1036HOST: 'local.example.com'1037}1038});1039});10401041test('out-of-order key resolution (#248550)', () => {1042const expr = ConfigurationResolverExpression.parse({1043'${input:key}': "${input:value}",1044});10451046for (const r of expr.unresolved()) {1047if (r.arg === 'key') {1048expr.resolve(r, 'the-key');1049}1050}1051for (const r of expr.unresolved()) {1052if (r.arg === 'value') {1053expr.resolve(r, 'the-value');1054}1055}10561057assert.deepStrictEqual(expr.toObject(), {1058'the-key': 'the-value'1059});1060});1061});106210631064