Add auto replies test

This commit is contained in:
Daniel Imms 2021-12-13 16:20:34 -08:00
parent 3cd011fa9f
commit 9566e299dd
9 changed files with 80 additions and 16 deletions

View file

@ -589,11 +589,6 @@ export interface ITerminalChildProcess {
updateProperty<T extends ProcessPropertyType>(property: T, value: IProcessPropertyMap[T]): Promise<void>;
}
export interface ITerminalEventListener {
handleInput(data: string): void;
handleResize(cols: number, rows: number): void;
}
export interface IReconnectConstants {
graceTime: number;
shortGraceTime: number;

View file

@ -6,14 +6,14 @@
import { timeout } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { isWindows } from 'vs/base/common/platform';
import { ITerminalChildProcess, ITerminalEventListener } from 'vs/platform/terminal/common/terminal';
import { ITerminalChildProcess } from 'vs/platform/terminal/common/terminal';
/**
* Tracks a terminal process's data stream and responds immediately when a matching string is
* received. This is done in a low overhead way and is ideally run on the same process as the
* where the process is handled to minimize latency.
*/
export class TerminalAutoResponder extends Disposable implements ITerminalEventListener {
export class TerminalAutoResponder extends Disposable {
private _pointer = 0;
private _paused = false;
private _throttled = false;
@ -30,6 +30,7 @@ export class TerminalAutoResponder extends Disposable implements ITerminalEventL
return;
}
const data = typeof e === 'string' ? e : e.data;
console.log('data ' + data);
for (let i = 0; i < data.length; i++) {
if (data[i] === matchWord[this._pointer]) {
this._pointer++;

View file

@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri';
import { getSystemShell } from 'vs/base/node/shell';
import { ILogService } from 'vs/platform/log/common/log';
import { RequestStore } from 'vs/platform/terminal/common/requestStore';
import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, ProcessCapability, ITerminalEventListener } from 'vs/platform/terminal/common/terminal';
import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, ProcessCapability } from 'vs/platform/terminal/common/terminal';
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment';
import { Terminal as XtermTerminal } from 'xterm-headless';
@ -426,8 +426,7 @@ interface IPersistentTerminalProcessLaunchOptions {
export class PersistentTerminalProcess extends Disposable {
private readonly _bufferer: TerminalDataBufferer;
private _eventListeners: ITerminalEventListener[] = [];
private _autoReplies: Map<string, TerminalAutoResponder> = new Map();
private readonly _autoReplies: Map<string, TerminalAutoResponder> = new Map();
private readonly _pendingCommands = new Map<number, { resolve: (data: any) => void; reject: (err: any) => void; }>();
@ -620,8 +619,8 @@ export class PersistentTerminalProcess extends Disposable {
if (this._inReplay) {
return;
}
for (const listener of this._eventListeners) {
listener.handleInput(data);
for (const listener of this._autoReplies.values()) {
listener.handleInput();
}
return this._terminalProcess.input(data);
}
@ -637,8 +636,8 @@ export class PersistentTerminalProcess extends Disposable {
// Buffered events should flush when a resize occurs
this._bufferer.flushBuffer(this._persistentProcessId);
for (const listener of this._eventListeners) {
listener.handleResize(cols, rows);
for (const listener of this._autoReplies.values()) {
listener.handleResize();
}
return this._terminalProcess.resize(cols, rows);
}

View file

@ -120,6 +120,8 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args);
case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]);
case '$installAutoReply': return this._ptyService.installAutoReply.apply(this._ptyService, args);
case '$uninstallAllAutoReplies': return this._ptyService.uninstallAllAutoReplies.apply(this._ptyService, args);
case '$getDefaultSystemShell': return this._getDefaultSystemShell.apply(this, args);
case '$getProfiles': return this._getProfiles.apply(this, args);
case '$getEnvironment': return this._getEnvironment();

View file

@ -166,6 +166,22 @@ class RemoteTerminalBackend extends Disposable implements ITerminalBackend {
const result = await Promise.all(resolveCalls);
channel.acceptPtyHostResolvedVariables(e.requestId, result);
}));
// Listen for config changes
const initialConfig = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(initialConfig.autoReplies)) {
channel.installAutoReply(match, initialConfig.autoReplies[match]);
}
// TODO: Could simplify update to a single call
this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(TerminalSettingId.AutoReplies)) {
channel.uninstallAllAutoReplies();
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(config.autoReplies)) {
await channel.installAutoReply(match, config.autoReplies[match]);
}
}
}));
} else {
this._remoteTerminalChannel = null;
}

View file

@ -243,6 +243,12 @@ export class RemoteTerminalChannelClient {
return this._channel.call('$sendCommandResult', [reqId, isError, payload]);
}
installAutoReply(match: string, reply: string): Promise<void> {
return this._channel.call('$installAutoReply', [match, reply]);
}
uninstallAllAutoReplies(): Promise<void> {
return this._channel.call('$uninstallAllAutoReplies', []);
}
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> {
return this._channel.call('$getDefaultSystemShell', [osOverride]);
}

View file

@ -82,9 +82,11 @@ export class Terminal {
await this.quickinput.waitForQuickInputClosed();
}
async runCommandInTerminal(commandText: string): Promise<void> {
async runCommandInTerminal(commandText: string, skipEnter?: boolean): Promise<void> {
await this.code.writeInTerminal(Selector.Xterm, commandText);
await this.code.dispatchKeybinding('enter');
if (!skipEnter) {
await this.code.dispatchKeybinding('enter');
}
}
async assertEditorGroupCount(count: number): Promise<void> {

View file

@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Application, Terminal, TerminalCommandId, SettingsEditor } from '../../../../automation';
export function setup() {
describe('Terminal Input', () => {
let terminal: Terminal;
let settingsEditor: SettingsEditor;
// Acquire automation API
before(async function () {
const app = this.app as Application;
terminal = app.workbench.terminal;
settingsEditor = app.workbench.settingsEditor;
});
describe('Auto replies', () => {
async function writeTextForAutoReply(text: string): Promise<void> {
// Put the matching word in quotes to avoid powershell coloring the first word and
// on a new line to avoid cursor move/line switching sequences
await terminal.runCommandInTerminal(`"\r${text}`, true);
}
it('should automatically reply to default "Terminate batch job (Y/N)"', async () => {
await terminal.runCommand(TerminalCommandId.CreateNew);
await writeTextForAutoReply('Terminate batch job (Y/N)?');
await terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Terminate batch job (Y/N)?Y')));
});
it('should automatically reply to a custom entry', async () => {
await settingsEditor.addUserSetting('terminal.integrated.autoReplies', '{ "foo": "bar" }');
await terminal.runCommand(TerminalCommandId.CreateNew);
await writeTextForAutoReply('foo');
await terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('foobar')));
});
});
});
}

View file

@ -6,6 +6,7 @@
import { Application, Terminal, TerminalCommandId, Logger } from '../../../../automation';
import { installAllHandlers } from '../../utils';
import { setup as setupTerminalEditorsTests } from './terminal-editors.test';
import { setup as setupTerminalInputTests } from './terminal-input.test';
import { setup as setupTerminalPersistenceTests } from './terminal-persistence.test';
import { setup as setupTerminalProfileTests } from './terminal-profiles.test';
import { setup as setupTerminalTabsTests } from './terminal-tabs.test';
@ -38,6 +39,7 @@ export function setup(logger: Logger) {
});
setupTerminalEditorsTests();
setupTerminalInputTests();
setupTerminalPersistenceTests();
setupTerminalProfileTests();
setupTerminalTabsTests();