Path: blob/main/src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts
5237 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 { type Terminal, type TerminalShellExecution, type TerminalShellExecutionCommandLine, type TerminalShellExecutionStartEvent } from 'vscode';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';7import { InternalTerminalShellIntegration } from '../../common/extHostTerminalShellIntegration.js';8import { Emitter } from '../../../../base/common/event.js';9import { TerminalShellExecutionCommandLineConfidence } from '../../common/extHostTypes.js';10import { deepStrictEqual, notStrictEqual, strictEqual } from 'assert';11import type { URI } from '../../../../base/common/uri.js';12import { DeferredPromise } from '../../../../base/common/async.js';1314function cmdLine(value: string): TerminalShellExecutionCommandLine {15return Object.freeze({16confidence: TerminalShellExecutionCommandLineConfidence.High,17value,18isTrusted: true,19});20}21function asCmdLine(value: string | TerminalShellExecutionCommandLine): TerminalShellExecutionCommandLine {22if (typeof value === 'string') {23return cmdLine(value);24}25return value;26}27function vsc(data: string) {28return `\x1b]633;${data}\x07`;29}3031const testCommandLine = 'echo hello world';32const testCommandLine2 = 'echo goodbye world';3334interface ITrackedEvent {35type: 'start' | 'data' | 'end';36commandLine: string;37data?: string;38}3940suite('InternalTerminalShellIntegration', () => {41const store = ensureNoDisposablesAreLeakedInTestSuite();4243let si: InternalTerminalShellIntegration;44let terminal: Terminal;45let onDidStartTerminalShellExecution: Emitter<TerminalShellExecutionStartEvent>;46let trackedEvents: ITrackedEvent[];47let readIteratorsFlushed: Promise<void>[];4849async function startExecutionAwaitObject(commandLine: string | TerminalShellExecutionCommandLine, cwd?: URI): Promise<TerminalShellExecution> {50return await new Promise<TerminalShellExecution>(r => {51store.add(onDidStartTerminalShellExecution.event(e => {52r(e.execution);53}));54si.startShellExecution(asCmdLine(commandLine), cwd);55});56}5758async function endExecutionAwaitObject(commandLine: string | TerminalShellExecutionCommandLine): Promise<TerminalShellExecution> {59return await new Promise<TerminalShellExecution>(r => {60store.add(si.onDidRequestEndExecution(e => r(e.execution)));61si.endShellExecution(asCmdLine(commandLine), 0);62});63}6465async function emitData(data: string): Promise<void> {66// AsyncIterableObjects are initialized in a microtask, this doesn't matter in practice67// since the events will always come through in different events.68await new Promise<void>(r => queueMicrotask(r));69si.emitData(data);70}7172function assertTrackedEvents(expected: ITrackedEvent[]) {73deepStrictEqual(trackedEvents, expected);74}7576function assertNonDataTrackedEvents(expected: ITrackedEvent[]) {77deepStrictEqual(trackedEvents.filter(e => e.type !== 'data'), expected);78}7980function assertDataTrackedEvents(expected: ITrackedEvent[]) {81deepStrictEqual(trackedEvents.filter(e => e.type === 'data'), expected);82}8384setup(() => {85// eslint-disable-next-line local/code-no-any-casts86terminal = Symbol('testTerminal') as any;87onDidStartTerminalShellExecution = store.add(new Emitter());88si = store.add(new InternalTerminalShellIntegration(terminal, true, onDidStartTerminalShellExecution));8990trackedEvents = [];91readIteratorsFlushed = [];92store.add(onDidStartTerminalShellExecution.event(async e => {93trackedEvents.push({94type: 'start',95commandLine: e.execution.commandLine.value,96});97const stream = e.execution.read();98const readIteratorsFlushedDeferred = new DeferredPromise<void>();99readIteratorsFlushed.push(readIteratorsFlushedDeferred.p);100for await (const data of stream) {101trackedEvents.push({102type: 'data',103commandLine: e.execution.commandLine.value,104data,105});106}107readIteratorsFlushedDeferred.complete();108}));109store.add(si.onDidRequestEndExecution(e => trackedEvents.push({110type: 'end',111commandLine: e.execution.commandLine.value,112})));113});114115test('simple execution', async () => {116const execution = await startExecutionAwaitObject(testCommandLine);117deepStrictEqual(execution.commandLine.value, testCommandLine);118const execution2 = await endExecutionAwaitObject(testCommandLine);119strictEqual(execution2, execution);120121assertTrackedEvents([122{ commandLine: testCommandLine, type: 'start' },123{ commandLine: testCommandLine, type: 'end' },124]);125});126127test('different execution unexpectedly ended', async () => {128const execution1 = await startExecutionAwaitObject(testCommandLine);129const execution2 = await endExecutionAwaitObject(testCommandLine2);130strictEqual(execution1, execution2, 'when a different execution is ended, the one that started first should end');131132assertTrackedEvents([133{ commandLine: testCommandLine, type: 'start' },134// This looks weird, but it's the same execution behind the scenes, just the command135// line was updated136{ commandLine: testCommandLine2, type: 'end' },137]);138});139140test('no end event', async () => {141const execution1 = await startExecutionAwaitObject(testCommandLine);142const endedExecution = await new Promise<TerminalShellExecution>(r => {143store.add(si.onDidRequestEndExecution(e => r(e.execution)));144startExecutionAwaitObject(testCommandLine2);145});146strictEqual(execution1, endedExecution, 'when no end event is fired, the current execution should end');147148// Clean up disposables149await endExecutionAwaitObject(testCommandLine2);150await Promise.all(readIteratorsFlushed);151152assertTrackedEvents([153{ commandLine: testCommandLine, type: 'start' },154{ commandLine: testCommandLine, type: 'end' },155{ commandLine: testCommandLine2, type: 'start' },156{ commandLine: testCommandLine2, type: 'end' },157]);158});159160suite('executeCommand', () => {161test('^C to clear previous command', async () => {162const commandLine = 'foo';163const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);164const firstExecution = await startExecutionAwaitObject('^C');165notStrictEqual(firstExecution, apiRequestedExecution.value);166si.emitData('SIGINT');167si.endShellExecution(cmdLine('^C'), 0);168si.startShellExecution(cmdLine(commandLine), undefined);169await emitData('1');170await endExecutionAwaitObject(commandLine);171// IMPORTANT: We cannot reliably assert the order of data events here because flushing172// of the async iterator is asynchronous and could happen after the execution's end173// event fires if an execution is started immediately afterwards.174await Promise.all(readIteratorsFlushed);175176assertNonDataTrackedEvents([177{ commandLine: '^C', type: 'start' },178{ commandLine: '^C', type: 'end' },179{ commandLine, type: 'start' },180{ commandLine, type: 'end' },181]);182assertDataTrackedEvents([183{ commandLine: '^C', type: 'data', data: 'SIGINT' },184{ commandLine, type: 'data', data: '1' },185]);186});187188test('multi-line command line', async () => {189const commandLine = 'foo\nbar';190const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);191const startedExecution = await startExecutionAwaitObject('foo');192strictEqual(startedExecution, apiRequestedExecution.value);193194si.emitData('1');195si.emitData('2');196si.endShellExecution(cmdLine('foo'), 0);197si.startShellExecution(cmdLine('bar'), undefined);198si.emitData('3');199si.emitData('4');200const endedExecution = await endExecutionAwaitObject('bar');201strictEqual(startedExecution, endedExecution);202203assertTrackedEvents([204{ commandLine, type: 'start' },205{ commandLine, type: 'data', data: '1' },206{ commandLine, type: 'data', data: '2' },207{ commandLine, type: 'data', data: '3' },208{ commandLine, type: 'data', data: '4' },209{ commandLine, type: 'end' },210]);211});212213test('multi-line command with long second command', async () => {214const commandLine = 'echo foo\ncat << EOT\nline1\nline2\nline3\nEOT';215const subCommandLine1 = 'echo foo';216const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';217218const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);219const startedExecution = await startExecutionAwaitObject(subCommandLine1);220strictEqual(startedExecution, apiRequestedExecution.value);221222si.emitData(`${vsc('C')}foo`);223si.endShellExecution(cmdLine(subCommandLine1), 0);224si.startShellExecution(cmdLine(subCommandLine2), undefined);225si.emitData(`${vsc('C')}line1`);226si.emitData('line2');227si.emitData('line3');228const endedExecution = await endExecutionAwaitObject(subCommandLine2);229strictEqual(startedExecution, endedExecution);230231assertTrackedEvents([232{ commandLine, type: 'start' },233{ commandLine, type: 'data', data: `${vsc('C')}foo` },234{ commandLine, type: 'data', data: `${vsc('C')}line1` },235{ commandLine, type: 'data', data: 'line2' },236{ commandLine, type: 'data', data: 'line3' },237{ commandLine, type: 'end' },238]);239});240241test('multi-line command comment followed by long second command', async () => {242const commandLine = '# comment: foo\ncat << EOT\nline1\nline2\nline3\nEOT';243const subCommandLine1 = '# comment: foo';244const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';245246const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);247const startedExecution = await startExecutionAwaitObject(subCommandLine1);248strictEqual(startedExecution, apiRequestedExecution.value);249250si.emitData(`${vsc('C')}`);251si.endShellExecution(cmdLine(subCommandLine1), 0);252si.startShellExecution(cmdLine(subCommandLine2), undefined);253si.emitData(`${vsc('C')}line1`);254si.emitData('line2');255si.emitData('line3');256const endedExecution = await endExecutionAwaitObject(subCommandLine2);257strictEqual(startedExecution, endedExecution);258259assertTrackedEvents([260{ commandLine, type: 'start' },261{ commandLine, type: 'data', data: `${vsc('C')}` },262{ commandLine, type: 'data', data: `${vsc('C')}line1` },263{ commandLine, type: 'data', data: 'line2' },264{ commandLine, type: 'data', data: 'line3' },265{ commandLine, type: 'end' },266]);267});268269test('4 multi-line commands with output', async () => {270const commandLine = 'echo "\nfoo"\ngit commit -m "hello\n\nworld"\ncat << EOT\nline1\nline2\nline3\nEOT\n{\necho "foo"\n}';271const subCommandLine1 = 'echo "\nfoo"';272const subCommandLine2 = 'git commit -m "hello\n\nworld"';273const subCommandLine3 = 'cat << EOT\nline1\nline2\nline3\nEOT';274const subCommandLine4 = '{\necho "foo"\n}';275276const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);277const startedExecution = await startExecutionAwaitObject(subCommandLine1);278strictEqual(startedExecution, apiRequestedExecution.value);279280si.emitData(`${vsc('C')}foo`);281si.endShellExecution(cmdLine(subCommandLine1), 0);282si.startShellExecution(cmdLine(subCommandLine2), undefined);283si.emitData(`${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)`);284si.endShellExecution(cmdLine(subCommandLine2), 0);285si.startShellExecution(cmdLine(subCommandLine3), undefined);286si.emitData(`${vsc('C')}line1`);287si.emitData('line2');288si.emitData('line3');289si.endShellExecution(cmdLine(subCommandLine3), 0);290si.emitData(`${vsc('C')}foo`);291si.startShellExecution(cmdLine(subCommandLine4), undefined);292const endedExecution = await endExecutionAwaitObject(subCommandLine4);293strictEqual(startedExecution, endedExecution);294295assertTrackedEvents([296{ commandLine, type: 'start' },297{ commandLine, type: 'data', data: `${vsc('C')}foo` },298{ commandLine, type: 'data', data: `${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)` },299{ commandLine, type: 'data', data: `${vsc('C')}line1` },300{ commandLine, type: 'data', data: 'line2' },301{ commandLine, type: 'data', data: 'line3' },302{ commandLine, type: 'data', data: `${vsc('C')}foo` },303{ commandLine, type: 'end' },304]);305});306});307});308309310