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
3296 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
terminal = Symbol('testTerminal') as any;
87
onDidStartTerminalShellExecution = store.add(new Emitter());
88
si = store.add(new InternalTerminalShellIntegration(terminal, onDidStartTerminalShellExecution));
89
90
trackedEvents = [];
91
readIteratorsFlushed = [];
92
store.add(onDidStartTerminalShellExecution.event(async e => {
93
trackedEvents.push({
94
type: 'start',
95
commandLine: e.execution.commandLine.value,
96
});
97
const stream = e.execution.read();
98
const readIteratorsFlushedDeferred = new DeferredPromise<void>();
99
readIteratorsFlushed.push(readIteratorsFlushedDeferred.p);
100
for await (const data of stream) {
101
trackedEvents.push({
102
type: 'data',
103
commandLine: e.execution.commandLine.value,
104
data,
105
});
106
}
107
readIteratorsFlushedDeferred.complete();
108
}));
109
store.add(si.onDidRequestEndExecution(e => trackedEvents.push({
110
type: 'end',
111
commandLine: e.execution.commandLine.value,
112
})));
113
});
114
115
test('simple execution', async () => {
116
const execution = await startExecutionAwaitObject(testCommandLine);
117
deepStrictEqual(execution.commandLine.value, testCommandLine);
118
const execution2 = await endExecutionAwaitObject(testCommandLine);
119
strictEqual(execution2, execution);
120
121
assertTrackedEvents([
122
{ commandLine: testCommandLine, type: 'start' },
123
{ commandLine: testCommandLine, type: 'end' },
124
]);
125
});
126
127
test('different execution unexpectedly ended', async () => {
128
const execution1 = await startExecutionAwaitObject(testCommandLine);
129
const execution2 = await endExecutionAwaitObject(testCommandLine2);
130
strictEqual(execution1, execution2, 'when a different execution is ended, the one that started first should end');
131
132
assertTrackedEvents([
133
{ commandLine: testCommandLine, type: 'start' },
134
// This looks weird, but it's the same execution behind the scenes, just the command
135
// line was updated
136
{ commandLine: testCommandLine2, type: 'end' },
137
]);
138
});
139
140
test('no end event', async () => {
141
const execution1 = await startExecutionAwaitObject(testCommandLine);
142
const endedExecution = await new Promise<TerminalShellExecution>(r => {
143
store.add(si.onDidRequestEndExecution(e => r(e.execution)));
144
startExecutionAwaitObject(testCommandLine2);
145
});
146
strictEqual(execution1, endedExecution, 'when no end event is fired, the current execution should end');
147
148
// Clean up disposables
149
await endExecutionAwaitObject(testCommandLine2);
150
await Promise.all(readIteratorsFlushed);
151
152
assertTrackedEvents([
153
{ commandLine: testCommandLine, type: 'start' },
154
{ commandLine: testCommandLine, type: 'end' },
155
{ commandLine: testCommandLine2, type: 'start' },
156
{ commandLine: testCommandLine2, type: 'end' },
157
]);
158
});
159
160
suite('executeCommand', () => {
161
test('^C to clear previous command', async () => {
162
const commandLine = 'foo';
163
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
164
const firstExecution = await startExecutionAwaitObject('^C');
165
notStrictEqual(firstExecution, apiRequestedExecution.value);
166
si.emitData('SIGINT');
167
si.endShellExecution(cmdLine('^C'), 0);
168
si.startShellExecution(cmdLine(commandLine), undefined);
169
await emitData('1');
170
await endExecutionAwaitObject(commandLine);
171
// IMPORTANT: We cannot reliably assert the order of data events here because flushing
172
// of the async iterator is asynchronous and could happen after the execution's end
173
// event fires if an execution is started immediately afterwards.
174
await Promise.all(readIteratorsFlushed);
175
176
assertNonDataTrackedEvents([
177
{ commandLine: '^C', type: 'start' },
178
{ commandLine: '^C', type: 'end' },
179
{ commandLine, type: 'start' },
180
{ commandLine, type: 'end' },
181
]);
182
assertDataTrackedEvents([
183
{ commandLine: '^C', type: 'data', data: 'SIGINT' },
184
{ commandLine, type: 'data', data: '1' },
185
]);
186
});
187
188
test('multi-line command line', async () => {
189
const commandLine = 'foo\nbar';
190
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
191
const startedExecution = await startExecutionAwaitObject('foo');
192
strictEqual(startedExecution, apiRequestedExecution.value);
193
194
si.emitData('1');
195
si.emitData('2');
196
si.endShellExecution(cmdLine('foo'), 0);
197
si.startShellExecution(cmdLine('bar'), undefined);
198
si.emitData('3');
199
si.emitData('4');
200
const endedExecution = await endExecutionAwaitObject('bar');
201
strictEqual(startedExecution, endedExecution);
202
203
assertTrackedEvents([
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
});
212
213
test('multi-line command with long second command', async () => {
214
const commandLine = 'echo foo\ncat << EOT\nline1\nline2\nline3\nEOT';
215
const subCommandLine1 = 'echo foo';
216
const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';
217
218
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
219
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
220
strictEqual(startedExecution, apiRequestedExecution.value);
221
222
si.emitData(`${vsc('C')}foo`);
223
si.endShellExecution(cmdLine(subCommandLine1), 0);
224
si.startShellExecution(cmdLine(subCommandLine2), undefined);
225
si.emitData(`${vsc('C')}line1`);
226
si.emitData('line2');
227
si.emitData('line3');
228
const endedExecution = await endExecutionAwaitObject(subCommandLine2);
229
strictEqual(startedExecution, endedExecution);
230
231
assertTrackedEvents([
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
});
240
241
test('multi-line command comment followed by long second command', async () => {
242
const commandLine = '# comment: foo\ncat << EOT\nline1\nline2\nline3\nEOT';
243
const subCommandLine1 = '# comment: foo';
244
const subCommandLine2 = 'cat << EOT\nline1\nline2\nline3\nEOT';
245
246
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
247
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
248
strictEqual(startedExecution, apiRequestedExecution.value);
249
250
si.emitData(`${vsc('C')}`);
251
si.endShellExecution(cmdLine(subCommandLine1), 0);
252
si.startShellExecution(cmdLine(subCommandLine2), undefined);
253
si.emitData(`${vsc('C')}line1`);
254
si.emitData('line2');
255
si.emitData('line3');
256
const endedExecution = await endExecutionAwaitObject(subCommandLine2);
257
strictEqual(startedExecution, endedExecution);
258
259
assertTrackedEvents([
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
});
268
269
test('4 multi-line commands with output', async () => {
270
const commandLine = 'echo "\nfoo"\ngit commit -m "hello\n\nworld"\ncat << EOT\nline1\nline2\nline3\nEOT\n{\necho "foo"\n}';
271
const subCommandLine1 = 'echo "\nfoo"';
272
const subCommandLine2 = 'git commit -m "hello\n\nworld"';
273
const subCommandLine3 = 'cat << EOT\nline1\nline2\nline3\nEOT';
274
const subCommandLine4 = '{\necho "foo"\n}';
275
276
const apiRequestedExecution = si.requestNewShellExecution(cmdLine(commandLine), undefined);
277
const startedExecution = await startExecutionAwaitObject(subCommandLine1);
278
strictEqual(startedExecution, apiRequestedExecution.value);
279
280
si.emitData(`${vsc('C')}foo`);
281
si.endShellExecution(cmdLine(subCommandLine1), 0);
282
si.startShellExecution(cmdLine(subCommandLine2), undefined);
283
si.emitData(`${vsc('C')} 2 files changed, 61 insertions(+), 2 deletions(-)`);
284
si.endShellExecution(cmdLine(subCommandLine2), 0);
285
si.startShellExecution(cmdLine(subCommandLine3), undefined);
286
si.emitData(`${vsc('C')}line1`);
287
si.emitData('line2');
288
si.emitData('line3');
289
si.endShellExecution(cmdLine(subCommandLine3), 0);
290
si.emitData(`${vsc('C')}foo`);
291
si.startShellExecution(cmdLine(subCommandLine4), undefined);
292
const endedExecution = await endExecutionAwaitObject(subCommandLine4);
293
strictEqual(startedExecution, endedExecution);
294
295
assertTrackedEvents([
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
});
308
309