Skip to content

Commit 6162e45

Browse files
committed
Add shellIntegration ext api integration tests
Part of microsoft#145234
1 parent bcca025 commit 6162e45

File tree

4 files changed

+231
-3
lines changed

4 files changed

+231
-3
lines changed

extensions/vscode-api-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"taskPresentationGroup",
4545
"terminalDataWriteEvent",
4646
"terminalDimensions",
47+
"terminalShellIntegration",
4748
"tunnels",
4849
"testObserver",
4950
"textSearchProvider",
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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 { deepStrictEqual, notStrictEqual, ok, strictEqual } from 'assert';
7+
import { platform } from 'os';
8+
import { env, TerminalShellExecutionCommandLineConfidence, UIKind, window, type Disposable, type Terminal, type TerminalShellExecution, type TerminalShellExecutionCommandLine, type TerminalShellExecutionEndEvent, type TerminalShellIntegration } from 'vscode';
9+
import { assertNoRpc } from '../utils';
10+
11+
// Disable terminal tests:
12+
// - Web https://github.com/microsoft/vscode/issues/92826
13+
(env.uiKind === UIKind.Web ? suite.skip : suite)('vscode API - Terminal.shellIntegration', () => {
14+
const disposables: Disposable[] = [];
15+
16+
teardown(async () => {
17+
assertNoRpc();
18+
disposables.forEach(d => d.dispose());
19+
disposables.length = 0;
20+
});
21+
22+
function createTerminalAndWaitForShellIntegration(): Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }> {
23+
return new Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }>(resolve => {
24+
disposables.push(window.onDidChangeTerminalShellIntegration(e => {
25+
if (e.terminal === terminal) {
26+
resolve({
27+
terminal,
28+
shellIntegration: e.shellIntegration
29+
});
30+
}
31+
}));
32+
const terminal = window.createTerminal();
33+
terminal.show();
34+
});
35+
}
36+
37+
function executeCommandAsync(shellIntegration: TerminalShellIntegration, command: string, args?: string[]): { execution: Promise<TerminalShellExecution>; endEvent: Promise<TerminalShellExecutionEndEvent> } {
38+
return {
39+
execution: new Promise<TerminalShellExecution>(resolve => {
40+
// Await a short period as pwsh's first SI prompt can fail when launched in quick succession
41+
setTimeout(() => {
42+
if (args) {
43+
resolve(shellIntegration.executeCommand(command, args));
44+
} else {
45+
resolve(shellIntegration.executeCommand(command));
46+
}
47+
}, 500);
48+
}),
49+
endEvent: new Promise<TerminalShellExecutionEndEvent>(resolve => {
50+
disposables.push(window.onDidEndTerminalShellExecution(e => {
51+
if (e.shellIntegration === shellIntegration) {
52+
resolve(e);
53+
}
54+
}));
55+
})
56+
};
57+
}
58+
59+
function closeTerminalAsync(terminal: Terminal): Promise<void> {
60+
return new Promise<void>(resolve => {
61+
disposables.push(window.onDidCloseTerminal(e => {
62+
if (e === terminal) {
63+
resolve();
64+
}
65+
}));
66+
terminal.dispose();
67+
});
68+
}
69+
70+
test('window.onDidChangeTerminalShellIntegration should activate for the default terminal', async () => {
71+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
72+
ok(terminal.shellIntegration);
73+
ok(shellIntegration);
74+
await closeTerminalAsync(terminal);
75+
});
76+
77+
test('execution events should fire in order when a command runs', async () => {
78+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
79+
const events: string[] = [];
80+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
81+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
82+
83+
await executeCommandAsync(shellIntegration, 'echo hello').endEvent;
84+
85+
deepStrictEqual(events, ['start', 'end']);
86+
87+
await closeTerminalAsync(terminal);
88+
});
89+
90+
// TODO: Exit code and command line in end events is flaky currently, marker adjustments are
91+
// likely the cause which make end events fire with undefined command line and exit codes
92+
(platform() === 'win32' ? test.skip : test)('end execution event should report zero exit code for successful commands', async () => {
93+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
94+
const events: string[] = [];
95+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
96+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
97+
98+
const endEvent = await executeCommandAsync(shellIntegration, 'echo hello').endEvent;
99+
strictEqual(endEvent.exitCode, 0);
100+
101+
await closeTerminalAsync(terminal);
102+
});
103+
104+
// TODO: Exit code and command line in end events is flaky currently, marker adjustments are
105+
// likely the cause which make end events fire with undefined command line and exit codes
106+
(platform() === 'win32' ? test.skip : test)('end execution event should report non-zero exit code for failed commands', async () => {
107+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
108+
const events: string[] = [];
109+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
110+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
111+
112+
const endEvent = await executeCommandAsync(shellIntegration, 'fakecommand').endEvent;
113+
notStrictEqual(endEvent.exitCode, 0);
114+
115+
await closeTerminalAsync(terminal);
116+
});
117+
118+
test('TerminalShellExecution.read iterables should be available between the start and end execution events', async () => {
119+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
120+
const events: string[] = [];
121+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
122+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
123+
124+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
125+
for await (const _ of (await execution).read()) {
126+
events.push('data');
127+
}
128+
await endEvent;
129+
130+
ok(events.length >= 3);
131+
strictEqual(events[0], 'start');
132+
strictEqual(events.at(-1), 'end');
133+
for (let i = 1; i < events.length - 1; i++) {
134+
strictEqual(events[i], 'data', 'all middle events should be data');
135+
}
136+
137+
await closeTerminalAsync(terminal);
138+
});
139+
140+
test('TerminalShellExecution.read events should fire with contents of command', async () => {
141+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
142+
const events: string[] = [];
143+
144+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
145+
for await (const data of (await execution).read()) {
146+
events.push(data);
147+
}
148+
await endEvent;
149+
150+
ok(events.join('').includes('hello'));
151+
152+
await closeTerminalAsync(terminal);
153+
});
154+
155+
test('TerminalShellExecution.read events should give separate iterables per call', async () => {
156+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
157+
158+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
159+
const executionSync = await execution;
160+
const firstRead = executionSync.read();
161+
const secondRead = executionSync.read();
162+
163+
const [firstReadEvents, secondReadEvents] = await Promise.all([
164+
new Promise<string[]>(resolve => {
165+
(async () => {
166+
const events: string[] = [];
167+
for await (const data of firstRead) {
168+
events.push(data);
169+
}
170+
resolve(events);
171+
})();
172+
}),
173+
new Promise<string[]>(resolve => {
174+
(async () => {
175+
const events: string[] = [];
176+
for await (const data of secondRead) {
177+
events.push(data);
178+
}
179+
resolve(events);
180+
})();
181+
}),
182+
]);
183+
await endEvent;
184+
185+
ok(firstReadEvents.join('').includes('hello'));
186+
deepStrictEqual(firstReadEvents, secondReadEvents);
187+
188+
await closeTerminalAsync(terminal);
189+
});
190+
191+
// TODO: Exit code and command line in end events is flaky currently, marker adjustments are
192+
// likely the cause which make end events fire with undefined command line and exit codes
193+
(platform() === 'win32' ? test.skip : test)('executeCommand(commandLine)', async () => {
194+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
195+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
196+
const executionSync = await execution;
197+
const expectedCommandLine: TerminalShellExecutionCommandLine = {
198+
value: 'echo hello',
199+
isTrusted: true,
200+
confidence: TerminalShellExecutionCommandLineConfidence.High
201+
};
202+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
203+
await endEvent;
204+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
205+
await closeTerminalAsync(terminal);
206+
});
207+
208+
// TODO: Exit code and command line in end events is flaky currently, marker adjustments are
209+
// likely the cause which make end events fire with undefined command line and exit codes
210+
(platform() === 'win32' ? test.skip : test)('executeCommand(executable, args)', async () => {
211+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
212+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo', ['hello']);
213+
const executionSync = await execution;
214+
const expectedCommandLine: TerminalShellExecutionCommandLine = {
215+
value: 'echo "hello"',
216+
isTrusted: true,
217+
confidence: TerminalShellExecutionCommandLineConfidence.High
218+
};
219+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
220+
await endEvent;
221+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
222+
await closeTerminalAsync(terminal);
223+
});
224+
});

src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ class UnixPtyHeuristics extends Disposable {
485485
}));
486486
}
487487

488-
async handleCommandStart(options?: IHandleCommandOptions) {
488+
handleCommandStart(options?: IHandleCommandOptions) {
489489
this._hooks.commitCommandFinished();
490490

491491
const currentCommand = this._capability.currentCommand;
@@ -638,7 +638,7 @@ class WindowsPtyHeuristics extends Disposable {
638638
}
639639
}
640640

641-
async handleCommandStart() {
641+
handleCommandStart() {
642642
this._capability.currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
643643

644644
// On Windows track all cursor movements after the command start sequence

src/vs/workbench/api/common/extHostTerminalShellIntegration.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ class ShellExecutionDataStream extends Disposable {
292292
private _emitters: AsyncIterableEmitter<string>[] = [];
293293

294294
createIterable(): AsyncIterable<string> {
295-
const barrier = this._barrier = new Barrier();
295+
if (!this._barrier) {
296+
this._barrier = new Barrier();
297+
}
298+
const barrier = this._barrier;
296299
const iterable = new AsyncIterableObject<string>(async emitter => {
297300
this._emitters.push(emitter);
298301
await barrier.wait();

0 commit comments

Comments
 (0)