diff --git a/extension.bundle.ts b/extension.bundle.ts index 9941ab7282..9de6466297 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -48,9 +48,11 @@ export { DebugConfigurationBase } from './src/debugging/DockerDebugConfiguration export { ActivityMeasurementService } from './src/telemetry/ActivityMeasurementService'; export { ExperimentationTelemetry } from './src/telemetry/ExperimentationTelemetry'; export { DockerApiClient } from './src/docker/DockerApiClient'; +export { DockerContext, isNewContextType } from './src/docker/Contexts'; export { DockerContainer } from './src/docker/Containers'; export { DockerImage } from './src/docker/Images'; export { DockerNetwork } from './src/docker/Networks'; export { DockerVolume } from './src/docker/Volumes'; +export { CommandTemplate, selectCommandTemplate, defaultCommandTemplates } from './src/commands/selectCommandTemplate'; export * from 'vscode-azureextensionui'; diff --git a/package-lock.json b/package-lock.json index 50bed25952..c3bb68f454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11801,9 +11801,9 @@ } }, "vscode-azureextensiondev": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/vscode-azureextensiondev/-/vscode-azureextensiondev-0.4.0.tgz", - "integrity": "sha512-2Ztr9UmO/AiY4Sy9nlXsQMUfVO6OZtN8eUyqQS+9krgGohPdNbdoOlYkwqWWwc/aAK6M263Lf6gZcv6NCqLxpQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/vscode-azureextensiondev/-/vscode-azureextensiondev-0.4.1.tgz", + "integrity": "sha512-uQul8jKKOexMN7SJNTNm0YF93+xbKtxrgUm6fJFymk8iddE7R86K7dPOq9+VjvwUPMSQirZLYIE2PCRRCIXsoA==", "dev": true, "requires": { "azure-arm-resource": "^3.0.0-preview", @@ -12184,9 +12184,9 @@ } }, "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true, "optional": true }, @@ -12201,9 +12201,9 @@ } }, "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz", + "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==", "dev": true, "optional": true, "requires": { diff --git a/package.json b/package.json index a22cdb0b5e..e6c046a788 100644 --- a/package.json +++ b/package.json @@ -1418,6 +1418,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.build.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1450,6 +1461,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.run.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1482,6 +1504,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.runInteractive.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1514,6 +1547,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.attach.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1546,6 +1590,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.logs.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1578,6 +1633,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.composeUp.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1590,7 +1656,19 @@ "type": "string" } ], - "default": "docker-compose ${configurationFile} up ${detached} ${build}", + "default": [ + { + "label": "Compose Up", + "template": "docker-compose ${configurationFile} up ${detached} ${build}", + "contextTypes": [ + "moby" + ] + }, + { + "label": "Compose Up", + "template": "docker compose ${configurationFile} up ${detached}" + } + ], "description": "%vscode-docker.config.template.composeUp.description%" }, "docker.commands.composeDown": { @@ -1610,6 +1688,17 @@ "match": { "type": "string", "description": "%vscode-docker.config.template.composeDown.match%" + }, + "contextTypes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "moby", + "aci" + ] + }, + "description": "%vscode-docker.config.template.contextTypes.description%" } }, "required": [ @@ -1622,7 +1711,19 @@ "type": "string" } ], - "default": "docker-compose ${configurationFile} down", + "default": [ + { + "label": "Compose Down", + "template": "docker-compose ${configurationFile} down", + "contextTypes": [ + "moby" + ] + }, + { + "label": "Compose Down", + "template": "docker compose ${configurationFile} down" + } + ], "description": "%vscode-docker.config.template.composeDown.description%" }, "docker.containers.groupBy": { @@ -2636,7 +2737,7 @@ "typescript": "^3.9.7", "umd-compat-loader": "^2.1.2", "vsce": "^1.77.0", - "vscode-azureextensiondev": "^0.4.0", + "vscode-azureextensiondev": "^0.4.1", "vscode-nls-dev": "^3.3.2", "vscode-test": "^1.4.0", "webpack": "^4.43.0", diff --git a/package.nls.json b/package.nls.json index 783613ab7a..ab3dfb3565 100644 --- a/package.nls.json +++ b/package.nls.json @@ -125,6 +125,7 @@ "vscode-docker.config.template.composeDown.label": "The label displayed to the user.", "vscode-docker.config.template.composeDown.match": "The regular expression for choosing the right template. Checked against docker-compose YAML files, folder name, etc.", "vscode-docker.config.template.composeDown.description": "Command templates for `docker-compose down` commands.", + "vscode-docker.config.template.contextTypes.description": "The context types in which the command template applies. If undefined or empty, the template applies in all context types.", "vscode-docker.config.docker.explorerRefreshInterval": "Docker view refresh interval (milliseconds)", "vscode-docker.config.docker.containers.groupBy": "The property to use to group containers in Docker view: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, Repository, RepositoryName, RepositoryNameAndTag, State, Status, Tag, or None", "vscode-docker.config.docker.containers.description": "Any secondary properties to display for a container (an array). Possible elements include: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, Repository, RepositoryName, RepositoryNameAndTag, State, Status, and Tag", diff --git a/src/commands/selectCommandTemplate.ts b/src/commands/selectCommandTemplate.ts index 29f8d935d1..c03da2c25a 100644 --- a/src/commands/selectCommandTemplate.ts +++ b/src/commands/selectCommandTemplate.ts @@ -5,29 +5,39 @@ import * as vscode from 'vscode'; import { IActionContext, IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { ContextType } from '../docker/Contexts'; import { ext } from '../extensionVariables'; import { localize } from '../localize'; import { resolveVariables } from '../utils/resolveVariables'; -export type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown'; +type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown'; -type CommandTemplate = { +// Exported only for tests +export type CommandTemplate = { template: string, label: string, match?: string, + contextTypes?: ContextType[], }; // NOTE: the default templates are duplicated in package.json, since VSCode offers no way of looking up extension-level default settings // So, when modifying them here, be sure to modify them there as well! -const defaults: { [key in TemplateCommand]: CommandTemplate } = { +// Exported only for tests +export const defaultCommandTemplates: { [key in TemplateCommand]: CommandTemplate[] } = { /* eslint-disable no-template-curly-in-string */ - 'build': { label: 'Docker Build', template: 'docker build --pull --rm -f "${dockerfile}" -t ${tag} "${context}"' }, - 'run': { label: 'Docker Run', template: 'docker run --rm -d ${exposedPorts} ${tag}' }, - 'runInteractive': { label: 'Docker Run (Interactive)', template: 'docker run --rm -it ${exposedPorts} ${tag}' }, - 'attach': { label: 'Docker Attach', template: 'docker exec -it ${containerId} ${shellCommand}' }, - 'logs': { label: 'Docker Logs', template: 'docker logs -f ${containerId}' }, - 'composeUp': { label: 'Compose Up', template: 'docker-compose ${configurationFile} up ${detached} ${build}' }, - 'composeDown': { label: 'Compose Down', template: 'docker-compose ${configurationFile} down' }, + 'build': [{ label: 'Docker Build', template: 'docker build --pull --rm -f "${dockerfile}" -t ${tag} "${context}"' }], + 'run': [{ label: 'Docker Run', template: 'docker run --rm -d ${exposedPorts} ${tag}' }], + 'runInteractive': [{ label: 'Docker Run (Interactive)', template: 'docker run --rm -it ${exposedPorts} ${tag}' }], + 'attach': [{ label: 'Docker Attach', template: 'docker exec -it ${containerId} ${shellCommand}' }], + 'logs': [{ label: 'Docker Logs', template: 'docker logs -f ${containerId}' }], + 'composeUp': [ + { label: 'Compose Up', template: 'docker-compose ${configurationFile} up ${detached} ${build}', contextTypes: ['moby'] }, + { label: 'Compose Up', template: 'docker compose ${configurationFile} up ${detached}' }, + ], + 'composeDown': [ + { label: 'Compose Down', template: 'docker-compose ${configurationFile} down', contextTypes: ['moby'] }, + { label: 'Compose Down', template: 'docker compose ${configurationFile} down' }, + ], /* eslint-enable no-template-curly-in-string */ }; @@ -88,53 +98,68 @@ export async function selectComposeCommand(context: IActionContext, folder: vsco ); } -async function selectCommandTemplate(context: IActionContext, command: TemplateCommand, matchContext?: string[], folder?: vscode.WorkspaceFolder, additionalVariables?: { [key: string]: string }): Promise { - // Get the templates from settings +// Exported only for tests +export async function selectCommandTemplate(context: IActionContext, command: TemplateCommand, matchContext: string[], folder: vscode.WorkspaceFolder | undefined, additionalVariables: { [key: string]: string }): Promise { + // Get the current context type + const currentContextType = (await ext.dockerContextManager.getCurrentContext()).Type; + + // Get the configured settings values const config = vscode.workspace.getConfiguration('docker'); const templateSetting: CommandTemplate[] | string = config.get(`commands.${command}`); - let templates: CommandTemplate[]; + let settingsTemplates: CommandTemplate[]; - // Get template(s) from settings + // Get a template array from settings if (typeof (templateSetting) === 'string') { - templates = [{ template: templateSetting }] as CommandTemplate[]; + settingsTemplates = [{ template: templateSetting }] as CommandTemplate[]; } else if (!templateSetting) { // If templateSetting is some falsy value, make this an empty array so the hardcoded default above gets used - templates = []; + settingsTemplates = []; } else { - templates = templateSetting; + settingsTemplates = templateSetting; } - // Look for settings-defined template(s) with explicit match, that matches the context - const matchedTemplates = templates.filter(template => { - if (template.match) { - try { - const matcher = new RegExp(template.match, 'i'); - return matchContext.some(m => matcher.test(m)); - } catch { - // Don't wait - // eslint-disable-next-line @typescript-eslint/no-floating-promises - ext.ui.showWarningMessage(localize('vscode-docker.commands.selectCommandTemplate.invalidMatch', 'Invalid match expression for template \'{0}\'. This template will be skipped.', template.label)); - } - } + // Get a template array from hardcoded defaults + const hardcodedTemplates = defaultCommandTemplates[command]; - return false; - }); + // Build the template selection matrix. Settings-defined values are preferred over hardcoded, and constrained over unconstrained. + // Constrained templates have either `match` or `contextTypes`, and must match the constraints. + // Unconstrained templates have neither `match` nor `contextTypes`. + const templateMatrix: CommandTemplate[][] = []; + + // 0. Settings-defined templates with either `match` or `contextTypes`, that satisfy the constraints + templateMatrix.push(getConstrainedTemplates(settingsTemplates, matchContext, currentContextType)); - // Look for settings-defined template(s) with no explicit match - const universalTemplates = templates.filter(template => !template.match); + // 1. Settings-defined templates with neither `match` nor `contextTypes` + templateMatrix.push(getUnconstrainedTemplates(settingsTemplates)); - // Select from explicit match templates, if none then from settings-defined universal templates, if none then hardcoded default + // 2. Hardcoded templates with either `match` or `contextTypes`, that satisfy the constraints + templateMatrix.push(getConstrainedTemplates(hardcodedTemplates, matchContext, currentContextType)); + + // 3. Hardcoded templates with neither `match` nor `contextTypes` + templateMatrix.push(getUnconstrainedTemplates(hardcodedTemplates)); + + // Select the template to use let selectedTemplate: CommandTemplate; - if (matchedTemplates.length > 0) { - selectedTemplate = await quickPickTemplate(context, matchedTemplates); - } else if (universalTemplates.length > 0) { - selectedTemplate = await quickPickTemplate(context, universalTemplates); - } else { - selectedTemplate = defaults[command]; + for (const templates of templateMatrix) { + // Skip any empty group + if (templates.length === 0) { + continue; + } + + // Choose a template from the first non-empty group + // If only one matches there will be no prompt + selectedTemplate = await quickPickTemplate(context, templates); + break; + } + + if (!selectedTemplate) { + throw new Error(localize('vscode-docker.commands.selectCommandTemplate.noTemplate', 'No command template was found for command \'{0}\'', command)); } - context.telemetry.properties.isDefaultCommand = selectedTemplate.template === defaults[command].template ? 'true' : 'false'; + context.telemetry.properties.isDefaultCommand = hardcodedTemplates.some(t => t.template === selectedTemplate.template) ? 'true' : 'false'; context.telemetry.properties.isCommandRegexMatched = selectedTemplate.match ? 'true' : 'false'; + context.telemetry.properties.commandContextType = `[${selectedTemplate.contextTypes?.join(', ') ?? ''}]`; + context.telemetry.properties.currentContextType = currentContextType; return resolveVariables(selectedTemplate.template, folder, additionalVariables); } @@ -159,3 +184,48 @@ async function quickPickTemplate(context: IActionContext, templates: CommandTemp return selection.data; } + +function getConstrainedTemplates(templates: CommandTemplate[], matchContext: string[], currentContextType: ContextType): CommandTemplate[] { + return templates.filter(template => { + if (!template.contextTypes && !template.match) { + // If neither contextTypes nor match is defined, this is an unconstrained template + return false; + } + + return isContextTypeConstraintSatisfied(currentContextType, template.contextTypes) && + isMatchConstraintSatisfied(matchContext, template.match); + }); +} + +function getUnconstrainedTemplates(templates: CommandTemplate[]): CommandTemplate[] { + return templates.filter(template => { + // Both contextTypes and match must be falsy to make this an unconstrained template + return !template.contextTypes && !template.match; + }); +} + +function isContextTypeConstraintSatisfied(currentContextType: ContextType, templateContextTypes: ContextType[] | undefined): boolean { + if (!templateContextTypes) { + // If templateContextTypes is undefined or empty, it is automatically satisfied + return true; + } + + return templateContextTypes.some(tc => tc === currentContextType); +} + +function isMatchConstraintSatisfied(matchContext: string[], match: string | undefined): boolean { + if (!match) { + // If match is undefined or empty, it is automatically satisfied + return true; + } + + try { + const matcher = new RegExp(match, 'i'); + return matchContext.some(m => matcher.test(m)); + } catch { + // Don't wait + void ext.ui.showWarningMessage(localize('vscode-docker.commands.selectCommandTemplate.invalidMatch', 'Invalid match expression \'{0}\'. This template will be skipped.', match)); + } + + return false; +} diff --git a/src/docker/ContextManager.ts b/src/docker/ContextManager.ts index 93a9ffc529..fc774312e7 100644 --- a/src/docker/ContextManager.ts +++ b/src/docker/ContextManager.ts @@ -16,7 +16,7 @@ import { LineSplitter } from '../debugging/coreclr/lineSplitter'; import { ext } from '../extensionVariables'; import { AsyncLazy } from '../utils/lazy'; import { execAsync, spawnAsync } from '../utils/spawnAsync'; -import { DockerContext, DockerContextInspection } from './Contexts'; +import { DockerContext, DockerContextInspection, isNewContextType } from './Contexts'; import { DockerodeApiClient } from './DockerodeApiClient/DockerodeApiClient'; import { DockerServeClient } from './DockerServeClient/DockerServeClient'; @@ -116,7 +116,7 @@ export class DockerContextManager implements ContextManager, Disposable { void ext.dockerClient?.dispose(); // Create a new client - if (currentContext.Type === 'aci') { + if (isNewContextType(currentContext.Type)) { // Currently vscode-docker:aciContext vscode-docker:newSdkContext mean the same thing // But that probably won't be true in the future, so define both as separate concepts now await this.setVsCodeContext('vscode-docker:aciContext', true); @@ -258,8 +258,8 @@ export class DockerContextManager implements ContextManager, Disposable { let result: boolean = false; const contexts = await this.contextsCache.getValue(); - if (contexts.some(c => c.Type === 'aci')) { - // If there are any ACI contexts we automatically know it's the new CLI + if (contexts.some(c => isNewContextType(c.Type))) { + // If there are any new contexts we automatically know it's the new CLI result = true; } else { // Otherwise we look at the output of `docker serve --help` diff --git a/src/docker/Contexts.ts b/src/docker/Contexts.ts index 264d6eb3b6..ae27ee0463 100644 --- a/src/docker/Contexts.ts +++ b/src/docker/Contexts.ts @@ -5,11 +5,13 @@ import { DockerObject } from './Common'; +export type ContextType = 'aci' | 'moby'; + export interface DockerContext extends DockerObject { readonly Description?: string; readonly DockerEndpoint: string; readonly Current: boolean; - readonly Type: 'aci' | 'moby'; + readonly Type: ContextType; readonly Id: string; // Will be equal to Name for contexts @@ -19,3 +21,13 @@ export interface DockerContext extends DockerObject { export interface DockerContextInspection { readonly [key: string]: unknown; } + +export function isNewContextType(contextType: ContextType): boolean { + switch (contextType) { + case 'moby': + return false; + case 'aci': // ACI is new + default: // Anything else is likely a new context type as well + return true; + } +} diff --git a/test/commands/selectCommandTemplate.test.ts b/test/commands/selectCommandTemplate.test.ts new file mode 100644 index 0000000000..a5218a7e30 --- /dev/null +++ b/test/commands/selectCommandTemplate.test.ts @@ -0,0 +1,616 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { runWithSetting } from '../runWithSetting'; +import { CommandTemplate, selectCommandTemplate, defaultCommandTemplates, ext, DockerContext, isNewContextType } from '../../extension.bundle'; +import { TestInput } from 'vscode-azureextensiondev'; +import { IActionContext } from 'vscode-azureextensionui'; +import { testUserInput } from '../global.test'; +import assert = require('assert'); + +suite("(unit) selectCommandTemplate", () => { + test("One constrained from settings (match)", async () => { + const result = await runWithCommandSetting( + [ + { + // *Satisfied constraint (match) + label: 'test', + template: 'test', + match: 'test', + }, + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unconstrained + label: 'fail2', + template: 'fail', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + { + // Unconstrained hardcoded (value is test to assert isDefaultCommand == true) + // (If we try to choose here it will fail due to prompting unexpectedly) + label: 'fail4', + template: 'test', + } + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("One constrained from settings (contextTypes)", async () => { + const result = await runWithCommandSetting( + [ + { + // *Satisfied constraint (contextTypes + match) + label: 'test', + template: 'test', + match: 'test', + contextTypes: ['moby', 'aci'], + }, + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + contextTypes: ['moby', 'aci'], + }, + { + // Unconstrained + label: 'fail2', + template: 'fail', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[moby, aci]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Two constrained from settings", async () => { + const result = await runWithCommandSetting( + [ + { + // *Satisfied constraint (contextTypes) + label: 'test', + template: 'test', + contextTypes: ['moby'], + }, + { + // *Satisfied constraint (match) + label: 'test2', + template: 'test', + match: 'test', + }, + { + // Unconstrained + label: 'fail', + template: 'fail', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail2', + template: 'fail', + }, + ], + [TestInput.UseDefaultValue], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("One unconstrained from settings", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + { + // *Unconstrained + label: 'test', + template: 'test', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Two unconstrained from settings", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + { + // *Unconstrained + label: 'test', + template: 'test', + }, + { + // *Unconstrained + label: 'test2', + template: 'test', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [TestInput.UseDefaultValue], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("One constrained from hardcoded (match)", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + ], + [ + { + // *Satisfied constraint (match) hardcoded + label: 'test', + template: 'test', + match: 'test', + }, + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("One constrained from hardcoded (contextTypes)", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + ], + [ + { + // *Satisfied constraint (contextTypes + match) hardcoded + label: 'test', + template: 'test', + match: 'test', + contextTypes: ['moby'], + }, + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[moby]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Two constrained from hardcoded", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + ], + [ + { + // *Satisfied constraint (contextTypes + match) hardcoded + label: 'test', + template: 'test', + match: 'test', + contextTypes: ['moby'], + }, + { + // *Satisfied constraint (match) hardcoded + label: 'test2', + template: 'test', + match: 'test', + }, + { + // Unconstrained hardcoded + label: 'fail3', + template: 'fail', + }, + ], + [TestInput.UseDefaultValue], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("One unconstrained from hardcoded", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + ], + [ + { + // Unsatisfied constraint (match) hardcoded + label: 'fail3', + template: 'fail', + match: 'fail', + contextTypes: ['moby'], + }, + { + // Unsatisfied constraint (contextTypes) hardcoded + label: 'fail4', + template: 'fail', + contextTypes: ['aci'] + }, + { + // *Unconstrained hardcoded + label: 'test', + template: 'test', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Two unconstrained from hardcoded", async () => { + const result = await runWithCommandSetting( + [ + { + // Unsatisfied constraint (match) + label: 'fail', + template: 'fail', + match: 'fail', + }, + { + // Unsatisfied constraint (contextTypes) + label: 'fail2', + template: 'fail', + contextTypes: ['aci'], + }, + ], + [ + { + // Unsatisfied constraint (match) hardcoded + label: 'fail3', + template: 'fail', + match: 'fail', + contextTypes: ['moby'], + }, + { + // Unsatisfied constraint (contextTypes) hardcoded + label: 'fail4', + template: 'fail', + contextTypes: ['aci'] + }, + { + // *Unconstrained hardcoded + label: 'test', + template: 'test', + }, + { + // *Unconstrained hardcoded + label: 'test2', + template: 'test', + }, + ], + [TestInput.UseDefaultValue], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Setting is a string", async () => { + const result = await runWithCommandSetting( + // *String setting + 'test', + [ + { + // Unconstrained hardcoded + label: 'fail', + template: 'fail', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Setting is falsy", async () => { + const result = await runWithCommandSetting( + [], // Falsy setting + [ + { + // *Unconstrained hardcoded + label: 'test', + template: 'test', + }, + ], + [], + 'moby', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'true', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'moby', 'Wrong value for currentContextType'); + }); + + test("Unknown context constrained", async () => { + const result = await runWithCommandSetting( + [ + { + // *Satisfied constraint (match) + label: 'test', + template: 'test', + match: 'test', + }, + { + // Unconstrained + label: 'fail', + template: 'fail', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail2', + template: 'fail', + }, + ], + [], + 'abc', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'true', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'abc', 'Wrong value for currentContextType'); + }); + + test("Unknown context unconstrained", async () => { + const result = await runWithCommandSetting( + [ + { + // *Unconstrained + label: 'test', + template: 'test', + }, + ], + [ + { + // Unconstrained hardcoded + label: 'fail', + template: 'fail', + }, + ], + [], + 'abc', + ['test'] + ); + + assert.equal(result.command, 'test', 'Incorrect command selected'); + + // Quick aside: validate that the context manager thinks an unknown context is new + assert.equal(isNewContextType('abc' as any), true, 'Incorrect context type identification'); + assert.equal(result.context.telemetry.properties.isDefaultCommand, 'false', 'Wrong value for isDefaultCommand'); + assert.equal(result.context.telemetry.properties.isCommandRegexMatched, 'false', 'Wrong value for isCommandRegexMatched'); + assert.equal(result.context.telemetry.properties.commandContextType, '[]', 'Wrong value for commandContextType'); + assert.equal(result.context.telemetry.properties.currentContextType, 'abc', 'Wrong value for currentContextType'); + }); +}); + +async function runWithCommandSetting( + settingsValues: CommandTemplate[] | string, + hardcodedValues: CommandTemplate[], + pickInputs: TestInput[], + contextType: string, + matchContext: string[]): Promise<{ command: string, context: IActionContext }> { + + const oldDefaultTemplates = defaultCommandTemplates['build']; + defaultCommandTemplates['build'] = hardcodedValues; + + const oldContextManager = ext.dockerContextManager; + ext.dockerContextManager = { + onContextChanged: undefined, + refresh: undefined, + getContexts: undefined, + inspect: undefined, + use: undefined, + remove: undefined, + isNewCli: undefined, + + // Only getCurrentContext is called by selectCommandTemplate + // From it, only Type is used + getCurrentContext: async () => { + return { + Type: contextType, + } as DockerContext; + }, + }; + + try { + const tempContext: IActionContext = { + telemetry: { properties: {}, measurements: {}, }, + errorHandling: { issueProperties: {}, }, + }; + + const cmdResult: string = await runWithSetting('commands.build', settingsValues, async () => { + return await testUserInput.runWithInputs(pickInputs, async () => { + return await selectCommandTemplate(tempContext, 'build', matchContext, undefined, {}); + }); + }); + + return { + command: cmdResult, + context: tempContext, + }; + } finally { + defaultCommandTemplates['build'] = oldDefaultTemplates; + ext.dockerContextManager = oldContextManager; + } +} diff --git a/test/runWithSetting.ts b/test/runWithSetting.ts index 41aa3a9b8f..01856e3970 100644 --- a/test/runWithSetting.ts +++ b/test/runWithSetting.ts @@ -6,13 +6,13 @@ import { ConfigurationTarget, workspace, WorkspaceConfiguration } from "vscode"; import { configPrefix } from "../extension.bundle"; -export async function runWithSetting(key: string, value: T | undefined, callback: () => Promise): Promise { +export async function runWithSetting(key: string, value: TSetting | undefined, callback: () => Promise): Promise { const config: WorkspaceConfiguration = workspace.getConfiguration(configPrefix); - const result = config.inspect(key); - const oldValue: T | undefined = result && result.globalValue; + const result = config.inspect(key); + const oldValue: TSetting | undefined = result && result.globalValue; try { await config.update(key, value, ConfigurationTarget.Global); - await callback(); + return await callback(); } finally { await config.update(key, oldValue, ConfigurationTarget.Global); }