Skip to content

Commit

Permalink
Add named pipes to pytest discovery and execution
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig committed Nov 1, 2023
1 parent 1e37c35 commit f7ce315
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 164 deletions.
70 changes: 35 additions & 35 deletions pythonFiles/vscode_pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
sys.path.append(os.fspath(script_dir / "lib" / "python"))

from typing import Any, Dict, List, Optional, Union

from testing_tools import socket_manager
from typing_extensions import Literal, TypedDict

DEFAULT_PORT = 45454

# import debugpy

# debugpy.connect(5678)
# debugpy.wait_for_client()


class TestData(TypedDict):
Expand Down Expand Up @@ -55,19 +57,17 @@ def __init__(self, message):
IS_DISCOVERY = False
map_id_to_path = dict()
collected_tests_so_far = list()
TEST_PORT = os.getenv("TEST_PORT")
TEST_UUID = os.getenv("TEST_UUID")
TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE")


def pytest_load_initial_conftests(early_config, parser, args):
global TEST_PORT
global TEST_UUID
TEST_PORT = os.getenv("TEST_PORT")
TEST_UUID = os.getenv("TEST_UUID")
global TEST_RUN_PIPE
TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE")
error_string = (
"PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being"
" changed or removed as they are required for successful test discovery and execution."
f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n"
"PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. "
"Please confirm this environment variable is not being changed or removed "
"as it is required for successful test discovery and execution."
f" \TEST_RUN_PIPE = {TEST_RUN_PIPE}\n"

Check warning on line 70 in pythonFiles/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Unsupported escape sequence in string literal (reportInvalidStringEscapeSequence)
)
print(error_string, file=sys.stderr)
if "--collect-only" in args:
Expand Down Expand Up @@ -636,8 +636,8 @@ def get_node_path(node: Any) -> pathlib.Path:
return getattr(node, "path", pathlib.Path(node.fspath))


__socket = None
atexit.register(lambda: __socket.close() if __socket else None)
__writer = None
atexit.register(lambda: __writer.close() if __writer else None)


def execution_post(
Expand Down Expand Up @@ -700,50 +700,50 @@ def send_post_request(
payload -- the payload data to be sent.
cls_encoder -- a custom encoder if needed.
"""
global TEST_PORT
global TEST_UUID
if TEST_UUID is None or TEST_PORT is None:
# if TEST_UUID or TEST_PORT is None, print an error and fail as these are both critical errors
if not TEST_RUN_PIPE:
error_msg = (
"PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being"
" changed or removed as they are required for successful pytest discovery and execution."
f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n"
"PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. "
"Please confirm this environment variable is not being changed or removed "
"as it is required for successful test discovery and execution."
f" \TEST_RUN_PIPE = {TEST_RUN_PIPE}\n"

Check warning on line 708 in pythonFiles/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Unsupported escape sequence in string literal (reportInvalidStringEscapeSequence)
)
print(error_msg, file=sys.stderr)
raise VSCodePytestError(error_msg)

addr = ("localhost", int(TEST_PORT))
global __socket
global __writer

if __socket is None:
if __writer is None:
try:
__socket = socket_manager.SocketManager(addr)
__socket.connect()
__writer = open(TEST_RUN_PIPE, "wt", encoding="utf-8")
except Exception as error:
error_msg = f"Error attempting to connect to extension communication socket[vscode-pytest]: {error}"
error_msg = f"Error attempting to connect to extension named pipe {TEST_RUN_PIPE}[vscode-pytest]: {error}"
print(error_msg, file=sys.stderr)
print(
"If you are on a Windows machine, this error may be occurring if any of your tests clear environment variables"
" as they are required to communicate with the extension. Please reference https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables"
"for the correct way to clear environment variables during testing.\n",
file=sys.stderr,
)
__socket = None
__writer = None
raise VSCodePytestError(error_msg)

data = json.dumps(payload, cls=cls_encoder)
request = f"""Content-Length: {len(data)}
Content-Type: application/json
Request-uuid: {TEST_UUID}
rpc = {
"jsonrpc": "2.0",
"params": payload,
}
data = json.dumps(rpc, cls=cls_encoder)
request = f"""content-length: {len(data)}
content-type: application/json
{data}"""

try:
if __socket is not None and __socket.socket is not None:
__socket.socket.sendall(request.encode("utf-8"))
if __writer:
__writer.write(request)
__writer.flush()
else:
print(
f"Plugin error connection error[vscode-pytest], socket is None \n[vscode-pytest] data: \n{request} \n",
f"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{request} \n",
file=sys.stderr,
)
except Exception as error:
Expand Down
46 changes: 9 additions & 37 deletions src/client/testing/testController/common/resultResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import {
import * as util from 'util';
import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types';
import { TestProvider } from '../../types';
import { traceError, traceLog } from '../../../logging';
import { traceError } from '../../../logging';
import { Testing } from '../../../common/utils/localize';
import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities';
import { sendTelemetryEvent } from '../../../telemetry';
import { EventName } from '../../../telemetry/constants';
import { splitLines } from '../../../common/stringUtils';
import { buildErrorNodeOptions, populateTestTree, splitTestNameWithRegex } from './utils';
import { Deferred } from '../../../common/utils/async';

export class PythonResultResolver implements ITestResultResolver {
testController: TestController;
Expand All @@ -45,28 +44,16 @@ export class PythonResultResolver implements ITestResultResolver {
this.vsIdToRunId = new Map<string, string>();
}

public resolveDiscovery(
payload: DiscoveredTestPayload | EOTTestPayload,
deferredTillEOT: Deferred<void>,
token?: CancellationToken,
): Promise<void> {
public resolveDiscovery(payload: DiscoveredTestPayload | EOTTestPayload, token?: CancellationToken): void {
if (!payload) {
// No test data is available
return Promise.resolve();
return;
}
if ('eot' in payload) {
// the payload is an EOT payload, so resolve the deferred promise.
traceLog('ResultResolver EOT received for discovery.');
const eotPayload = payload as EOTTestPayload;
if (eotPayload.eot === true) {
deferredTillEOT.resolve();
return Promise.resolve();
}
}
return this._resolveDiscovery(payload as DiscoveredTestPayload, token);

this._resolveDiscovery(payload as DiscoveredTestPayload, token);
}

public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise<void> {
public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void {
const workspacePath = this.workspaceUri.fsPath;
const rawTestData = payload as DiscoveredTestPayload;
// Check if there were any errors in the discovery process.
Expand Down Expand Up @@ -109,27 +96,13 @@ export class PythonResultResolver implements ITestResultResolver {
tool: this.testProvider,
failed: false,
});
return Promise.resolve();
}

public resolveExecution(
payload: ExecutionTestPayload | EOTTestPayload,
runInstance: TestRun,
deferredTillEOT: Deferred<void>,
): Promise<void> {
if (payload !== undefined && 'eot' in payload) {
// the payload is an EOT payload, so resolve the deferred promise.
traceLog('ResultResolver EOT received for execution.');
const eotPayload = payload as EOTTestPayload;
if (eotPayload.eot === true) {
deferredTillEOT.resolve();
return Promise.resolve();
}
}
return this._resolveExecution(payload as ExecutionTestPayload, runInstance);
public resolveExecution(payload: ExecutionTestPayload | EOTTestPayload, runInstance: TestRun): void {
this._resolveExecution(payload as ExecutionTestPayload, runInstance);
}

public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise<void> {
public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void {
const rawTestExecData = payload as ExecutionTestPayload;
if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) {
// Map which holds the subtest information for each test item.
Expand Down Expand Up @@ -279,6 +252,5 @@ export class PythonResultResolver implements ITestResultResolver {
}
}
}
return Promise.resolve();
}
}
17 changes: 4 additions & 13 deletions src/client/testing/testController/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from 'vscode';
import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types';
import { IPythonExecutionFactory } from '../../../common/process/types';
import { Deferred } from '../../../common/utils/async';
import { EnvironmentVariables } from '../../../common/variables/types';

export type TestRunInstanceOptions = TestRunOptions & {
Expand Down Expand Up @@ -195,18 +194,10 @@ export interface ITestResultResolver {
runIdToVSid: Map<string, string>;
runIdToTestItem: Map<string, TestItem>;
vsIdToRunId: Map<string, string>;
resolveDiscovery(
payload: DiscoveredTestPayload | EOTTestPayload,
deferredTillEOT: Deferred<void>,
token?: CancellationToken,
): Promise<void>;
resolveExecution(
payload: ExecutionTestPayload | EOTTestPayload,
runInstance: TestRun,
deferredTillEOT: Deferred<void>,
): Promise<void>;
_resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise<void>;
_resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise<void>;
resolveDiscovery(payload: DiscoveredTestPayload | EOTTestPayload, token?: CancellationToken): void;
resolveExecution(payload: ExecutionTestPayload | EOTTestPayload, runInstance: TestRun): void;
_resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void;
_resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void;
}
export interface ITestDiscoveryAdapter {
// ** first line old method signature, second line new method signature
Expand Down
82 changes: 81 additions & 1 deletion src/client/testing/testController/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.
import * as net from 'net';
import * as path from 'path';
import { CancellationToken, Position, TestController, TestItem, Uri, Range } from 'vscode';
import { CancellationToken, Position, TestController, TestItem, Uri, Range, Disposable } from 'vscode';
import { Message } from 'vscode-jsonrpc';
import { traceError, traceLog, traceVerbose } from '../../../logging';

Expand Down Expand Up @@ -187,6 +187,86 @@ export async function startTestIdsNamedPipe(testIds: string[]): Promise<string>
return pipeName;
}

interface ExecutionResultMessage extends Message {
params: ExecutionTestPayload | EOTTestPayload;
}

export async function startRunResultNamedPipe(
callback: (payload: ExecutionTestPayload | EOTTestPayload) => void,
cancellationToken?: CancellationToken,
): Promise<{ name: string } & Disposable> {
const pipeName: string = generateRandomPipeName('python-test-results');
const server = await createNamedPipeServer(pipeName);
let dispose: () => void = () => {
/* noop */
};
server.onConnected().then(([reader, _writer]) => {
traceVerbose(`Test Result named pipe ${pipeName} connected`);
let disposables: (Disposable | undefined)[] = [reader];
dispose = () => {
traceVerbose(`Test Result named pipe ${pipeName} disposed`);
disposables.forEach((d) => d?.dispose());
disposables = [];
};
disposables.push(
cancellationToken?.onCancellationRequested(() => {
traceVerbose(`Test Result named pipe ${pipeName} cancelled`);
dispose();
}),
reader.listen((data: Message) => {
traceVerbose(`Test Result named pipe ${pipeName} received data`);
callback((data as ExecutionResultMessage).params as ExecutionTestPayload | EOTTestPayload);
}),
reader.onClose(() => {
callback(createEOTPayload(true));
traceVerbose(`Test Result named pipe ${pipeName} closed`);
dispose();
}),
);
});
return { name: pipeName, dispose };
}

interface DiscoveryResultMessage extends Message {
params: DiscoveredTestPayload | EOTTestPayload;
}

export async function startDiscoveryNamedPipe(
callback: (payload: DiscoveredTestPayload | EOTTestPayload) => void,
cancellationToken?: CancellationToken,
): Promise<{ name: string } & Disposable> {
const pipeName: string = generateRandomPipeName('python-test-discovery');
const server = await createNamedPipeServer(pipeName);
let dispose: () => void = () => {
/* noop */
};
server.onConnected().then(([reader, _writer]) => {
traceVerbose(`Test Discovery named pipe ${pipeName} connected`);
let disposables: (Disposable | undefined)[] = [reader];
dispose = () => {
traceVerbose(`Test Discovery named pipe ${pipeName} disposed`);
disposables.forEach((d) => d?.dispose());
disposables = [];
};
disposables.push(
cancellationToken?.onCancellationRequested(() => {
traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`);
dispose();
}),
reader.listen((data: Message) => {
traceVerbose(`Test Discovery named pipe ${pipeName} received data`);
callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload | EOTTestPayload);
}),
reader.onClose(() => {
callback(createEOTPayload(true));
traceVerbose(`Test Discovery named pipe ${pipeName} closed`);
dispose();
}),
);
});
return { name: pipeName, dispose };
}

export async function startTestIdServer(testIds: string[]): Promise<number> {
const startServer = (): Promise<number> =>
new Promise((resolve, reject) => {
Expand Down
2 changes: 0 additions & 2 deletions src/client/testing/testController/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,12 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
testProvider = PYTEST_PROVIDER;
resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri);
discoveryAdapter = new PytestTestDiscoveryAdapter(
this.pythonTestServer,
this.configSettings,
this.testOutputChannel,
resultResolver,
this.envVarsService,
);
executionAdapter = new PytestTestExecutionAdapter(
this.pythonTestServer,
this.configSettings,
this.testOutputChannel,
resultResolver,
Expand Down
Loading

0 comments on commit f7ce315

Please sign in to comment.