Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts
5241 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { type Terminal, type TerminalShellExecution, type TerminalShellExecutionCommandLine, type TerminalShellExecutionStartEvent } from 'vscode';
7
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
8
import { InternalTerminalShellIntegration } from '../../common/extHostTerminalShellIntegration.js';
9
import { Emitter } from '../../../../base/common/event.js';
10
import { TerminalShellExecutionCommandLineConfidence } from '../../common/extHostTypes.js';
11
import { deepStrictEqual, notStrictEqual, strictEqual } from 'assert';
12
import type { URI } from '../../../../base/common/uri.js';
13
import { DeferredPromise } from '../../../../base/common/async.js';
14
15
function cmdLine(value: string): TerminalShellExecutionCommandLine {
16
return Object.freeze({
17
confidence: TerminalShellExecutionCommandLineConfidence.High,
18
value,
19
isTrusted: true,
20
});
21
}
22
function asCmdLine(value: string | TerminalShellExecutionCommandLine): TerminalShellExecutionCommandLine {
23
if (typeof value === 'string') {
24
return cmdLine(value);
25
}
26
return value;
27
}
28
function vsc(data: string) {
29
return `\x1b]633;${data}\x07`;
30
}
31
32
const testCommandLine = 'echo hello world';
33
const testCommandLine2 = 'echo goodbye world';
34
35
interface ITrackedEvent {
36
type: 'start' | 'data' | 'end';
37
commandLine: string;
38
data?: string;
39
}
40
41
suite('InternalTerminalShellIntegration', () => {
42
const store = ensureNoDisposablesAreLeakedInTestSuite();
43
44
let si: InternalTerminalShellIntegration;
45
let terminal: Terminal;
46
let onDidStartTerminalShellExecution: Emitter<TerminalShellExecutionStartEvent>;
47
let trackedEvents: ITrackedEvent[];
48
let readIteratorsFlushed: Promise<void>[];
49
50
async function startExecutionAwaitObject(commandLine: string | TerminalShellExecutionCommandLine, cwd?: URI): Promise<TerminalShellExecution> {
51
return await new Promise<TerminalShellExecution>(r => {
52
store.add(onDidStartTerminalShellExecution.event(e => {
53
r(e.execution);
54
}));
55
si.startShellExecution(asCmdLine(commandLine), cwd);
56
});
57
}
58
59
async function endExecutionAwaitObject(commandLine: string | TerminalShellExecutionCommandLine): Promise<TerminalShellExecution> {
60
return await new Promise<TerminalShellExecution>(r => {
61
store.add(si.onDidRequestEndExecution(e => r(e.execution)));
62
si.endShellExecution(asCmdLine(commandLine), 0);
63
});
64
}
65
66
async function emitData(data: string): Promise<void> {
67
// AsyncIterableObjects are initialized in a microtask, this doesn't matter in practice
68
// since the events will always come through in different events.
69
await new Promise<void>(r => queueMicrotask(r));
70
si.emitData(data);
71
}
72
73
function assertTrackedEvents(expected: ITrackedEvent[]) {
74
deepStrictEqual(trackedEvents, expected);
75
}
76
77
function assertNonDataTrackedEvents(expected: ITrackedEvent[]) {
78
deepStrictEqual(trackedEvents.filter(e => e.type !== 'data'), expected);
79
}
80
81
function assertDataTrackedEvents(expected: ITrackedEvent[]) {
82
deepStrictEqual(trackedEvents.filter(e => e.type === 'data'), expected);
83
}
84
85
setup(() => {
86
// eslint-disable-next-line local/code-no-any-casts
87
terminal = Symbol('testTerminal') as any;
88
onDidStartTerminalShellExecution = store.add(new Emitter());
89
si = store.add(new InternalTerminalShellIntegration(terminal, true, onDidStartTerminalShellExecution));
90
91
trackedEvents = [];
92
readIteratorsFlushed = [];
93
store.add(onDidStartTerminalShellExecution.event(async e => {
94
trackedEvents.push({
95
type: 'start',
96
commandLine: e.execution.commandLine.value,
97
});
98
const stream = e.execution.read();
99
const readIteratorsFlushedDeferred = new DeferredPromise<void>();
100
readIteratorsFlushed.push(readIteratorsFlushedDeferred.p);
101
for await (const data of stream) {
102
trackedEvents.push({
103
type: 'data',
104
commandLine: e.execution.commandLine.value,
105
data,
106
});
107
}
108
readIteratorsFlushedDeferred.complete();
109
}));
110
store.add(si.onDidRequestEndExecution(e => trackedEvents.push({
111
type: 'end',
112
commandLine: e.execution.commandLine.value,
113
})));
114
});
115
116
test('simple execution', async () => {
117
const execution = await startExecutionAwaitObject(testCommandLine);
118
deepStrictEqual(execution.commandLine.value, testCommandLine);
119
const execution2 = await endExecutionAwaitObject(testCommandLine);
120
strictEqual(execution2, execution);
121
122
assertTrackedEvents([
123
{ commandLine: testCommandLine, type: 'start' },
124
{ commandLine: testCommandLine, type: 'end' },
125
]);
126
});
127
128
test('different execution unexpectedly ended', async () => {
129
const execution1 = await startExecutionAwaitObject(testCommandLine);
130
const execution2 = await endExecutionAwaitObject(testCommandLine2);
131
strictEqual(execution1, execution2, 'when a different execution is ended, the one that started first should end');
132
133
assertTrackedEvents([
134
{ commandLine: testCommandLine, type: 'start' },
135
// This looks weird, but it's the same execution behind the scenes, just the command
136
// line was updated
137
{ commandLine: testCommandLine2, type: 'end' },
138
]);
139
});
140
141
test('no end event', async () => {
142
const execution1 = await startExecutionAwaitObject(testCommandLine);
143
const endedExecution = await new Promise<TerminalShellExecution>(r => {
144
store.add(si.onDidRequestEndExecution(e => r(e.execution)));
145
startExecutionAwaitObject(testCommandLine2);
146
});
147
strictEqual(execution1, endedExecution, 'when no end event is fired, the current execution should end');
148
149
// Clean up disposables
150
await endExecutionAwaitObject(testCommandLine2);
151
await Promise.all(readIteratorsFlushed);
152
153
assertTrackedEvents([
154
{ commandLine: testCommandLine, type: 'start' },
155
{ commandLine: testCommandLine, type: 'end' },
156
{ commandLine: testCommandLine2, type: 'start' },
157
{ commandLine: testCommandLine2, type: 'end' },
158
]);
159
});
160
161
suite('executeCommand', () => {
162
test('^C to clear previous command', async () => {
163
const commandLine = 'foo';
164
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
165
const firstExecution = await startExecutionAwaitObject('^C');
166
notStrictEqual(firstExecution, apiRequestedExecution.value);
167
si.emitData('SIGINT');
168
si.endShellExecution(cmdLine('^C'), 0);
169
si.startShellExecution(cmdLine(commandLine), undefined);
170
await emitData('1');
171
await endExecutionAwaitObject(commandLine);
172
// IMPORTANT: We cannot reliably assert the order of data events here because flushing
173
// of the async iterator is asynchronous and could happen after the execution's end
174
// event fires if an execution is started immediately afterwards.
175
await Promise.all(readIteratorsFlushed);
176
177
assertNonDataTrackedEvents([
178
{ commandLine: '^C', type: 'start' },
179
{ commandLine: '^C', type: 'end' },
180
{ commandLine, type: 'start' },
181
{ commandLine, type: 'end' },
182
]);
183
assertDataTrackedEvents([
184
{ commandLine: '^C', type: 'data', data: 'SIGINT' },
185
{ commandLine, type: 'data', data: '1' },
186
]);
187
});
188
189
test('multi-line command line', async () => {
190
const commandLine = 'foo\nbar';
191
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
192
const startedExecution = await startExecutionAwaitObject('foo');
193
strictEqual(startedExecution, apiRequestedExecution.value);
194
195
si.emitData('1');
196
si.emitData('2');
197
si.endShellExecution(cmdLine('foo'), 0);
198
si.startShellExecution(cmdLine('bar'), undefined);
199
si.emitData('3');
200
si.emitData('4');
201
const endedExecution = await endExecutionAwaitObject('bar');
202
strictEqual(startedExecution, endedExecution);
203
204
assertTrackedEvents([
205
{ commandLine, type: 'start' },
206
{ commandLine, type: 'data', data: '1' },
207
{ commandLine, type: 'data', data: '2' },
208
{ commandLine, type: 'data', data: '3' },
209
{ commandLine, type: 'data', data: '4' },
210
{ commandLine, type: 'end' },
211
]);
212
});
213
214
test('multi-line command with long second command', async () => {
215
const commandLine = 'echo foo\ncat << EOT\nline1\nline2\nline3\nEOT';
216
const subCommandLine1 = 'echo foo';
217
const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';
218
219
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
220
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
221
strictEqual(startedExecution, apiRequestedExecution.value);
222
223
si.emitData(`${vsc('C')}foo`);
224
si.endShellExecution(cmdLine(subCommandLine1), 0);
225
si.startShellExecution(cmdLine(subCommandLine2), undefined);
226
si.emitData(`${vsc('C')}line1`);
227
si.emitData('line2');
228
si.emitData('line3');
229
const endedExecution = await endExecutionAwaitObject(subCommandLine2);
230
strictEqual(startedExecution, endedExecution);
231
232
assertTrackedEvents([
233
{ commandLine, type: 'start' },
234
{ commandLine, type: 'data', data: `${vsc('C')}foo` },
235
{ commandLine, type: 'data', data: `${vsc('C')}line1` },
236
{ commandLine, type: 'data', data: 'line2' },
237
{ commandLine, type: 'data', data: 'line3' },
238
{ commandLine, type: 'end' },
239
]);
240
});
241
242
test('multi-line command comment followed by long second command', async () => {
243
const commandLine = '# comment: foo\ncat << EOT\nline1\nline2\nline3\nEOT';
244
const subCommandLine1 = '# comment: foo';
245
const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';
246
247
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
248
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
249
strictEqual(startedExecution, apiRequestedExecution.value);
250
251
si.emitData(`${vsc('C')}`);
252
si.endShellExecution(cmdLine(subCommandLine1), 0);
253
si.startShellExecution(cmdLine(subCommandLine2), undefined);
254
si.emitData(`${vsc('C')}line1`);
255
si.emitData('line2');
256
si.emitData('line3');
257
const endedExecution = await endExecutionAwaitObject(subCommandLine2);
258
strictEqual(startedExecution, endedExecution);
259
260
assertTrackedEvents([
261
{ commandLine, type: 'start' },
262
{ commandLine, type: 'data', data: `${vsc('C')}` },
263
{ commandLine, type: 'data', data: `${vsc('C')}line1` },
264
{ commandLine, type: 'data', data: 'line2' },
265
{ commandLine, type: 'data', data: 'line3' },
266
{ commandLine, type: 'end' },
267
]);
268
});
269
270
test('4 multi-line commands with output', async () => {
271
const commandLine = 'echo "\nfoo"\ngit commit -m "hello\n\nworld"\ncat << EOT\nline1\nline2\nline3\nEOT\n{\necho "foo"\n}';
272
const subCommandLine1 = 'echo "\nfoo"';
273
const subCommandLine2 = 'git commit -m "hello\n\nworld"';
274
const subCommandLine3 = 'cat << EOT\nline1\nline2\nline3\nEOT';
275
const subCommandLine4 = '{\necho "foo"\n}';
276
277
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
278
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
279
strictEqual(startedExecution, apiRequestedExecution.value);
280
281
si.emitData(`${vsc('C')}foo`);
282
si.endShellExecution(cmdLine(subCommandLine1), 0);
283
si.startShellExecution(cmdLine(subCommandLine2), undefined);
284
si.emitData(`${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)`);
285
si.endShellExecution(cmdLine(subCommandLine2), 0);
286
si.startShellExecution(cmdLine(subCommandLine3), undefined);
287
si.emitData(`${vsc('C')}line1`);
288
si.emitData('line2');
289
si.emitData('line3');
290
si.endShellExecution(cmdLine(subCommandLine3), 0);
291
si.emitData(`${vsc('C')}foo`);
292
si.startShellExecution(cmdLine(subCommandLine4), undefined);
293
const endedExecution = await endExecutionAwaitObject(subCommandLine4);
294
strictEqual(startedExecution, endedExecution);
295
296
assertTrackedEvents([
297
{ commandLine, type: 'start' },
298
{ commandLine, type: 'data', data: `${vsc('C')}foo` },
299
{ commandLine, type: 'data', data: `${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)` },
300
{ commandLine, type: 'data', data: `${vsc('C')}line1` },
301
{ commandLine, type: 'data', data: 'line2' },
302
{ commandLine, type: 'data', data: 'line3' },
303
{ commandLine, type: 'data', data: `${vsc('C')}foo` },
304
{ commandLine, type: 'end' },
305
]);
306
});
307
});
308
});
309
310