Path: blob/main/src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts
3296 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(() => {85terminal = Symbol('testTerminal') as any;86onDidStartTerminalShellExecution = store.add(new Emitter());87si = store.add(new InternalTerminalShellIntegration(terminal, onDidStartTerminalShellExecution));8889trackedEvents = [];90readIteratorsFlushed = [];91store.add(onDidStartTerminalShellExecution.event(async e => {92trackedEvents.push({93type: 'start',94commandLine: e.execution.commandLine.value,95});96const stream = e.execution.read();97const readIteratorsFlushedDeferred = new DeferredPromise<void>();98readIteratorsFlushed.push(readIteratorsFlushedDeferred.p);99for await (const data of stream) {100trackedEvents.push({101type: 'data',102commandLine: e.execution.commandLine.value,103data,104});105}106readIteratorsFlushedDeferred.complete();107}));108store.add(si.onDidRequestEndExecution(e => trackedEvents.push({109type: 'end',110commandLine: e.execution.commandLine.value,111})));112});113114test('simple execution', async () => {115const execution = await startExecutionAwaitObject(testCommandLine);116deepStrictEqual(execution.commandLine.value, testCommandLine);117const execution2 = await endExecutionAwaitObject(testCommandLine);118strictEqual(execution2, execution);119120assertTrackedEvents([121{ commandLine: testCommandLine, type: 'start' },122{ commandLine: testCommandLine, type: 'end' },123]);124});125126test('different execution unexpectedly ended', async () => {127const execution1 = await startExecutionAwaitObject(testCommandLine);128const execution2 = await endExecutionAwaitObject(testCommandLine2);129strictEqual(execution1, execution2, 'when a different execution is ended, the one that started first should end');130131assertTrackedEvents([132{ commandLine: testCommandLine, type: 'start' },133// This looks weird, but it's the same execution behind the scenes, just the command134// line was updated135{ commandLine: testCommandLine2, type: 'end' },136]);137});138139test('no end event', async () => {140const execution1 = await startExecutionAwaitObject(testCommandLine);141const endedExecution = await new Promise<TerminalShellExecution>(r => {142store.add(si.onDidRequestEndExecution(e => r(e.execution)));143startExecutionAwaitObject(testCommandLine2);144});145strictEqual(execution1, endedExecution, 'when no end event is fired, the current execution should end');146147// Clean up disposables148await endExecutionAwaitObject(testCommandLine2);149await Promise.all(readIteratorsFlushed);150151assertTrackedEvents([152{ commandLine: testCommandLine, type: 'start' },153{ commandLine: testCommandLine, type: 'end' },154{ commandLine: testCommandLine2, type: 'start' },155{ commandLine: testCommandLine2, type: 'end' },156]);157});158159suite('executeCommand', () => {160test('^C to clear previous command', async () => {161const commandLine = 'foo';162const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);163const firstExecution = await startExecutionAwaitObject('^C');164notStrictEqual(firstExecution, apiRequestedExecution.value);165si.emitData('SIGINT');166si.endShellExecution(cmdLine('^C'), 0);167si.startShellExecution(cmdLine(commandLine), undefined);168await emitData('1');169await endExecutionAwaitObject(commandLine);170// IMPORTANT: We cannot reliably assert the order of data events here because flushing171// of the async iterator is asynchronous and could happen after the execution's end172// event fires if an execution is started immediately afterwards.173await Promise.all(readIteratorsFlushed);174175assertNonDataTrackedEvents([176{ commandLine: '^C', type: 'start' },177{ commandLine: '^C', type: 'end' },178{ commandLine, type: 'start' },179{ commandLine, type: 'end' },180]);181assertDataTrackedEvents([182{ commandLine: '^C', type: 'data', data: 'SIGINT' },183{ commandLine, type: 'data', data: '1' },184]);185});186187test('multi-line command line', async () => {188const commandLine = 'foo\nbar';189const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);190const startedExecution = await startExecutionAwaitObject('foo');191strictEqual(startedExecution, apiRequestedExecution.value);192193si.emitData('1');194si.emitData('2');195si.endShellExecution(cmdLine('foo'), 0);196si.startShellExecution(cmdLine('bar'), undefined);197si.emitData('3');198si.emitData('4');199const endedExecution = await endExecutionAwaitObject('bar');200strictEqual(startedExecution, endedExecution);201202assertTrackedEvents([203{ commandLine, type: 'start' },204{ commandLine, type: 'data', data: '1' },205{ commandLine, type: 'data', data: '2' },206{ commandLine, type: 'data', data: '3' },207{ commandLine, type: 'data', data: '4' },208{ commandLine, type: 'end' },209]);210});211212test('multi-line command with long second command', async () => {213const commandLine = 'echo foo\ncat << EOT\nline1\nline2\nline3\nEOT';214const subCommandLine1 = 'echo foo';215const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';216217const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);218const startedExecution = await startExecutionAwaitObject(subCommandLine1);219strictEqual(startedExecution, apiRequestedExecution.value);220221si.emitData(`${vsc('C')}foo`);222si.endShellExecution(cmdLine(subCommandLine1), 0);223si.startShellExecution(cmdLine(subCommandLine2), undefined);224si.emitData(`${vsc('C')}line1`);225si.emitData('line2');226si.emitData('line3');227const endedExecution = await endExecutionAwaitObject(subCommandLine2);228strictEqual(startedExecution, endedExecution);229230assertTrackedEvents([231{ commandLine, type: 'start' },232{ commandLine, type: 'data', data: `${vsc('C')}foo` },233{ commandLine, type: 'data', data: `${vsc('C')}line1` },234{ commandLine, type: 'data', data: 'line2' },235{ commandLine, type: 'data', data: 'line3' },236{ commandLine, type: 'end' },237]);238});239240test('multi-line command comment followed by long second command', async () => {241const commandLine = '# comment: foo\ncat << EOT\nline1\nline2\nline3\nEOT';242const subCommandLine1 = '# comment: foo';243const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';244245const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);246const startedExecution = await startExecutionAwaitObject(subCommandLine1);247strictEqual(startedExecution, apiRequestedExecution.value);248249si.emitData(`${vsc('C')}`);250si.endShellExecution(cmdLine(subCommandLine1), 0);251si.startShellExecution(cmdLine(subCommandLine2), undefined);252si.emitData(`${vsc('C')}line1`);253si.emitData('line2');254si.emitData('line3');255const endedExecution = await endExecutionAwaitObject(subCommandLine2);256strictEqual(startedExecution, endedExecution);257258assertTrackedEvents([259{ commandLine, type: 'start' },260{ commandLine, type: 'data', data: `${vsc('C')}` },261{ commandLine, type: 'data', data: `${vsc('C')}line1` },262{ commandLine, type: 'data', data: 'line2' },263{ commandLine, type: 'data', data: 'line3' },264{ commandLine, type: 'end' },265]);266});267268test('4 multi-line commands with output', async () => {269const commandLine = 'echo "\nfoo"\ngit commit -m "hello\n\nworld"\ncat << EOT\nline1\nline2\nline3\nEOT\n{\necho "foo"\n}';270const subCommandLine1 = 'echo "\nfoo"';271const subCommandLine2 = 'git commit -m "hello\n\nworld"';272const subCommandLine3 = 'cat << EOT\nline1\nline2\nline3\nEOT';273const subCommandLine4 = '{\necho "foo"\n}';274275const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);276const startedExecution = await startExecutionAwaitObject(subCommandLine1);277strictEqual(startedExecution, apiRequestedExecution.value);278279si.emitData(`${vsc('C')}foo`);280si.endShellExecution(cmdLine(subCommandLine1), 0);281si.startShellExecution(cmdLine(subCommandLine2), undefined);282si.emitData(`${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)`);283si.endShellExecution(cmdLine(subCommandLine2), 0);284si.startShellExecution(cmdLine(subCommandLine3), undefined);285si.emitData(`${vsc('C')}line1`);286si.emitData('line2');287si.emitData('line3');288si.endShellExecution(cmdLine(subCommandLine3), 0);289si.emitData(`${vsc('C')}foo`);290si.startShellExecution(cmdLine(subCommandLine4), undefined);291const endedExecution = await endExecutionAwaitObject(subCommandLine4);292strictEqual(startedExecution, endedExecution);293294assertTrackedEvents([295{ commandLine, type: 'start' },296{ commandLine, type: 'data', data: `${vsc('C')}foo` },297{ commandLine, type: 'data', data: `${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)` },298{ commandLine, type: 'data', data: `${vsc('C')}line1` },299{ commandLine, type: 'data', data: 'line2' },300{ commandLine, type: 'data', data: 'line3' },301{ commandLine, type: 'data', data: `${vsc('C')}foo` },302{ commandLine, type: 'end' },303]);304});305});306});307308309