Make command templates context type aware (#2176)

* Extend cmd customization for context types

* Add unit tests for command template selection
This commit is contained in:
Brandon Waterloo [MSFT] 2020-07-23 11:43:18 -04:00 коммит произвёл GitHub
Родитель 33fd749ed1
Коммит d7f9027de6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 866 добавлений и 64 удалений

Просмотреть файл

@ -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';

18
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": {

Просмотреть файл

@ -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",

Просмотреть файл

@ -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",

Просмотреть файл

@ -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<string> {
// 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<string> {
// 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];
// 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));
// 1. Settings-defined templates with neither `match` nor `contextTypes`
templateMatrix.push(getUnconstrainedTemplates(settingsTemplates));
// 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;
for (const templates of templateMatrix) {
// Skip any empty group
if (templates.length === 0) {
continue;
}
return false;
});
// Look for settings-defined template(s) with no explicit match
const universalTemplates = templates.filter(template => !template.match);
// Select from explicit match templates, if none then from settings-defined universal templates, if none then hardcoded default
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];
// Choose a template from the first non-empty group
// If only one matches there will be no prompt
selectedTemplate = await quickPickTemplate(context, templates);
break;
}
context.telemetry.properties.isDefaultCommand = selectedTemplate.template === defaults[command].template ? 'true' : 'false';
if (!selectedTemplate) {
throw new Error(localize('vscode-docker.commands.selectCommandTemplate.noTemplate', 'No command template was found for command \'{0}\'', command));
}
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;
}

Просмотреть файл

@ -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`

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -6,13 +6,13 @@
import { ConfigurationTarget, workspace, WorkspaceConfiguration } from "vscode";
import { configPrefix } from "../extension.bundle";
export async function runWithSetting<T>(key: string, value: T | undefined, callback: () => Promise<void>): Promise<void> {
export async function runWithSetting<TSetting, TCallback>(key: string, value: TSetting | undefined, callback: () => Promise<TCallback>): Promise<TCallback> {
const config: WorkspaceConfiguration = workspace.getConfiguration(configPrefix);
const result = config.inspect<T>(key);
const oldValue: T | undefined = result && result.globalValue;
const result = config.inspect<TSetting>(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);
}