Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent first Python command being lost #22902

Merged
merged 34 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
72eb85c
prevent first command swallow
anthonykim1 Feb 8, 2024
8861db4
Add application shell
anthonykim1 Feb 12, 2024
e6dfa92
add application shell to djangoShellCodeExec
anthonykim1 Feb 12, 2024
3effe72
try increasing time to prevent timeout
anthonykim1 Feb 13, 2024
a56b7dd
Try to prevent with 1 second
anthonykim1 Feb 13, 2024
f71e75c
different timeout
anthonykim1 Feb 13, 2024
550b381
try get rid of timeout
anthonykim1 Feb 13, 2024
b8ccd0b
try switching order
anthonykim1 Feb 13, 2024
73cd561
add fallback for tests
anthonykim1 Feb 13, 2024
feca5ba
fixing test
anthonykim1 Feb 13, 2024
60a61d8
fixing test
anthonykim1 Feb 13, 2024
aa23d90
try if increasing time helps
anthonykim1 Feb 13, 2024
b9ae762
not using protected readonly
anthonykim1 Feb 13, 2024
3ed8991
check how original passes
anthonykim1 Feb 13, 2024
25a17bf
back to working state
anthonykim1 Feb 13, 2024
eb4e658
back to original timeout limit
anthonykim1 Feb 13, 2024
965476b
stop double calling sendCommand
anthonykim1 Feb 13, 2024
13b808c
remove duplicate repl
anthonykim1 Feb 13, 2024
caaeb4d
Update test to count for new Promise race
anthonykim1 Feb 13, 2024
41461eb
add comments
anthonykim1 Feb 15, 2024
45616dc
testing old test
anthonykim1 Feb 15, 2024
d508a98
adjust timeout
anthonykim1 Feb 16, 2024
0597446
testing
anthonykim1 Feb 16, 2024
e09f8ac
adjust timeout and comment
anthonykim1 Feb 16, 2024
61dcd8c
adjust timeout
anthonykim1 Feb 16, 2024
4596a95
switch promise
anthonykim1 Feb 16, 2024
ca79f34
try new test
anthonykim1 Feb 16, 2024
35d25fe
lint
anthonykim1 Feb 16, 2024
d2b2eda
add more test
anthonykim1 Feb 16, 2024
9e8ce00
try testing
anthonykim1 Feb 16, 2024
60f9448
update test
anthonykim1 Feb 16, 2024
38a2ea2
clean up
anthonykim1 Feb 16, 2024
160150d
more cleanup
anthonykim1 Feb 16, 2024
676e885
re-arrange
anthonykim1 Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Disposable, Uri } from 'vscode';
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types';
import {
IApplicationShell,
ICommandManager,
IDocumentManager,
IWorkspaceService,
} from '../../common/application/types';
import '../../common/extensions';
import { IFileSystem, IPlatformService } from '../../common/platform/types';
import { ITerminalServiceFactory } from '../../common/terminal/types';
Expand All @@ -28,6 +33,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
@inject(IFileSystem) fileSystem: IFileSystem,
@inject(IDisposableRegistry) disposableRegistry: Disposable[],
@inject(IInterpreterService) interpreterService: IInterpreterService,
@inject(IApplicationShell) applicationShell: IApplicationShell,
) {
super(
terminalServiceFactory,
Expand All @@ -37,6 +43,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
platformService,
interpreterService,
commandManager,
applicationShell,
);
this.terminalTitle = 'Django Shell';
disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager));
Expand Down
4 changes: 3 additions & 1 deletion src/client/terminals/codeExecution/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { inject, injectable } from 'inversify';
import { Disposable } from 'vscode';
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
import { IPlatformService } from '../../common/platform/types';
import { ITerminalServiceFactory } from '../../common/terminal/types';
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
Expand All @@ -22,6 +22,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
@inject(IPlatformService) platformService: IPlatformService,
@inject(IInterpreterService) interpreterService: IInterpreterService,
@inject(ICommandManager) commandManager: ICommandManager,
@inject(IApplicationShell) applicationShell: IApplicationShell,
) {
super(
terminalServiceFactory,
Expand All @@ -31,6 +32,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
platformService,
interpreterService,
commandManager,
applicationShell,
);
this.terminalTitle = 'REPL';
}
Expand Down
35 changes: 30 additions & 5 deletions src/client/terminals/codeExecution/terminalCodeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Disposable, Uri } from 'vscode';
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { IPlatformService } from '../../common/platform/types';
import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types';
import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types';
import { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../common/types';
import { Diagnostics, Repl } from '../../common/utils/localize';
import { showWarningMessage } from '../../common/vscodeApis/windowApis';
import { IInterpreterService } from '../../interpreter/contracts';
Expand All @@ -30,6 +30,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
@inject(IPlatformService) protected readonly platformService: IPlatformService,
@inject(IInterpreterService) protected readonly interpreterService: IInterpreterService,
@inject(ICommandManager) protected readonly commandManager: ICommandManager,
@inject(IApplicationShell) protected readonly applicationShell: IApplicationShell,
) {}

public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) {
Expand Down Expand Up @@ -65,10 +66,34 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
}
this.replActive = new Promise<boolean>(async (resolve) => {
const replCommandArgs = await this.getExecutableInfo(resource);
let listener: IDisposable;
Promise.race([
new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 3000)),
new Promise<boolean>((resolve) => {
let count = 0;
const terminalDataTimeout = setTimeout(() => {
resolve(true); // Fall back for test case scenarios.
Copy link
Member

@hediet hediet Feb 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 71 seems to do this already.
Generally, this code seems to be quite difficult to read (a for loop in an event listener in a promise that is returned by a promise), I suggest to reduce nesting a bit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback!! @hediet.
It's little tricky right now, since I wanted a promise that always returned true, maximum three second or faster if we see that Python REPL has launched before.
I definitely thought about removing the second timeout, but in testing scenario, I would get promise rejection since it could not read the ">>>" from TerminalData like how it would normally read from user's terminal when we launch Python REPL from shell of their choice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but in testing scenario, I would get promise rejection

I don't understand why this would happen. Are you sure? race ignores future rejections.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I found it pretty odd too.. Perhaps its because during testing, we don't have "mocking" of Terminal write data containing the ">>>" that I check for to see if Python REPL has launched?? hence maybe thats why it may be giving rejection before we pass with the 3 second condition.

}, 3000);
// Watch TerminalData to see if REPL launched.
listener = this.applicationShell.onDidWriteTerminalData((e) => {
for (let i = 0; i < e.data.length; i++) {
if (e.data[i] === '>') {
count++;
if (count === 3) {
clearTimeout(terminalDataTimeout);
resolve(true);
}
}
}
});
}),
]).then(() => {
if (listener) {
listener.dispose();
}
resolve(true);
});
terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args);

// Give python repl time to start before we start sending text.
setTimeout(() => resolve(true), 1000);
});
this.disposables.push(
terminalService.onDidCloseTerminal(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import * as path from 'path';
import * as TypeMoq from 'typemoq';
import * as sinon from 'sinon';
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types';
import {
IApplicationShell,
ICommandManager,
IDocumentManager,
IWorkspaceService,
} from '../../../client/common/application/types';
import { IFileSystem, IPlatformService } from '../../../client/common/platform/types';
import { createCondaEnv } from '../../../client/common/process/pythonEnvironment';
import { createPythonProcessService } from '../../../client/common/process/pythonProcess';
Expand All @@ -32,12 +37,14 @@ suite('Terminal - Django Shell Code Execution', () => {
let settings: TypeMoq.IMock<IPythonSettings>;
let interpreterService: TypeMoq.IMock<IInterpreterService>;
let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>;
let applicationShell: TypeMoq.IMock<IApplicationShell>;
let disposables: Disposable[] = [];
setup(() => {
const terminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>();
terminalService = TypeMoq.Mock.ofType<ITerminalService>();
const configService = TypeMoq.Mock.ofType<IConfigurationService>();
applicationShell = TypeMoq.Mock.ofType<IApplicationShell>();
workspace = TypeMoq.Mock.ofType<IWorkspaceService>();
workspace
.setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
Expand All @@ -62,6 +69,7 @@ suite('Terminal - Django Shell Code Execution', () => {
fileSystem.object,
disposables,
interpreterService.object,
applicationShell.object,
);

terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object);
Expand Down
52 changes: 23 additions & 29 deletions src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import * as path from 'path';
import { SemVer } from 'semver';
import * as TypeMoq from 'typemoq';
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types';
import {
IApplicationShell,
ICommandManager,
IDocumentManager,
IWorkspaceService,
} from '../../../client/common/application/types';
import { IFileSystem, IPlatformService } from '../../../client/common/platform/types';
import { createCondaEnv } from '../../../client/common/process/pythonEnvironment';
import { createPythonProcessService } from '../../../client/common/process/pythonProcess';
Expand Down Expand Up @@ -47,6 +52,7 @@ suite('Terminal - Code Execution', () => {
let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>;
let interpreterService: TypeMoq.IMock<IInterpreterService>;
let isDjangoRepl: boolean;
let applicationShell: TypeMoq.IMock<IApplicationShell>;

teardown(() => {
disposables.forEach((disposable) => {
Expand All @@ -71,6 +77,7 @@ suite('Terminal - Code Execution', () => {
fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>();
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
applicationShell = TypeMoq.Mock.ofType<IApplicationShell>();
settings = TypeMoq.Mock.ofType<IPythonSettings>();
settings.setup((s) => s.terminal).returns(() => terminalSettings.object);
configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object);
Expand All @@ -85,6 +92,7 @@ suite('Terminal - Code Execution', () => {
platform.object,
interpreterService.object,
commandManager.object,
applicationShell.object,
);
break;
}
Expand All @@ -97,6 +105,7 @@ suite('Terminal - Code Execution', () => {
platform.object,
interpreterService.object,
commandManager.object,
applicationShell.object,
);
expectedTerminalTitle = 'REPL';
break;
Expand All @@ -120,6 +129,7 @@ suite('Terminal - Code Execution', () => {
fileSystem.object,
disposables,
interpreterService.object,
applicationShell.object,
);
expectedTerminalTitle = 'Django Shell';
break;
Expand Down Expand Up @@ -590,7 +600,7 @@ suite('Terminal - Code Execution', () => {
);
});

test('Ensure repl is re-initialized when terminal is closed', async () => {
test('Ensure REPL launches after reducing risk of command being ignored or duplicated', async () => {
const pythonPath = 'usr/bin/python1234';
const terminalArgs = ['-a', 'b', 'c'];
platform.setup((p) => p.isWindows).returns(() => false);
Expand All @@ -599,43 +609,27 @@ suite('Terminal - Code Execution', () => {
.returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment));
terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs);

let closeTerminalCallback: undefined | (() => void);
terminalService
.setup((t) => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns((callback) => {
closeTerminalCallback = callback;
return {
dispose: noop,
};
});

await executor.execute('cmd1');
await executor.execute('cmd2');
await executor.execute('cmd3');

const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs;

expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized');
terminalService.verify(
async (t) =>
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
TypeMoq.Times.once(),
// Now check if sendCommand from the initializeRepl is called atLeastOnce.
// This is due to newly added Promise race and fallback to lower risk of swollen first command.
applicationShell.verify(
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
TypeMoq.Times.atLeastOnce(),
);

closeTerminalCallback!.call(terminalService.object);
await executor.execute('cmd4');
terminalService.verify(
async (t) =>
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
TypeMoq.Times.exactly(2),
applicationShell.verify(
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
TypeMoq.Times.atLeastOnce(),
);

closeTerminalCallback!.call(terminalService.object);
await executor.execute('cmd5');
terminalService.verify(
async (t) =>
t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)),
TypeMoq.Times.exactly(3),
applicationShell.verify(
async (t) => t.onDidWriteTerminalData(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
TypeMoq.Times.atLeastOnce(),
);
});

Expand Down
Loading