From 3ccf984f1dc4e849263d749c78ffeb9ab0d72118 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 6 Jan 2025 11:30:00 -0800 Subject: [PATCH] Remove fifo regression (#24685) fixes regression in https://github.com/microsoft/vscode-python/issues/24656 by reverting problem --- noxfile.py | 1 - python_files/testing_tools/socket_manager.py | 56 +++-- python_files/tests/pytestadapter/helpers.py | 33 +-- python_files/unittestadapter/pvsc_utils.py | 11 +- python_files/vscode_pytest/__init__.py | 43 ++-- python_files/vscode_pytest/_common.py | 2 - src/client/common/pipes/namedPipes.ts | 231 +++++------------- .../testing/testController/common/utils.ts | 140 ++++++----- .../pytest/pytestDiscoveryAdapter.ts | 9 +- .../pytest/pytestExecutionAdapter.ts | 29 ++- .../unittest/testDiscoveryAdapter.ts | 4 +- .../unittest/testExecutionAdapter.ts | 24 +- .../testing/common/testingAdapter.test.ts | 87 ++++++- .../pytestDiscoveryAdapter.unit.test.ts | 9 +- .../pytestExecutionAdapter.unit.test.ts | 9 +- .../testCancellationRunAdapters.unit.test.ts | 31 +-- .../testDiscoveryAdapter.unit.test.ts | 9 +- .../testExecutionAdapter.unit.test.ts | 9 +- .../errorWorkspace/test_seg_fault.py | 3 +- 19 files changed, 376 insertions(+), 364 deletions(-) delete mode 100644 python_files/vscode_pytest/_common.py diff --git a/noxfile.py b/noxfile.py index 3991ee8c025a..60e22d461074 100644 --- a/noxfile.py +++ b/noxfile.py @@ -53,7 +53,6 @@ def install_python_libs(session: nox.Session): ) session.install("packaging") - session.install("debugpy") # Download get-pip script session.run( diff --git a/python_files/testing_tools/socket_manager.py b/python_files/testing_tools/socket_manager.py index f143ac111cdb..347453a6ca1a 100644 --- a/python_files/testing_tools/socket_manager.py +++ b/python_files/testing_tools/socket_manager.py @@ -20,24 +20,39 @@ def __exit__(self, *_): self.close() def connect(self): - self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 - # reader created in read method + if sys.platform == "win32": + self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 + # reader created in read method + else: + self._socket = _SOCKET(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self.name) return self def close(self): - self._writer.close() - if hasattr(self, "_reader"): - self._reader.close() + if sys.platform == "win32": + self._writer.close() + else: + # add exception catch + self._socket.close() def write(self, data: str): - try: - # for windows, is should only use \n\n - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - self._writer.write(request) - self._writer.flush() - except Exception as e: - print("error attempting to write to pipe", e) - raise (e) + if sys.platform == "win32": + try: + # for windows, is should only use \n\n + request = ( + f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" + ) + self._writer.write(request) + self._writer.flush() + except Exception as e: + print("error attempting to write to pipe", e) + raise (e) + else: + # must include the carriage-return defined (as \r\n) for unix systems + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + self._socket.send(request.encode("utf-8")) def read(self, bufsize=1024) -> str: """Read data from the socket. @@ -48,10 +63,17 @@ def read(self, bufsize=1024) -> str: Returns: data (str): Data received from the socket. """ - # returns a string automatically from read - if not hasattr(self, "_reader"): - self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 - return self._reader.read(bufsize) + if sys.platform == "win32": + # returns a string automatically from read + if not hasattr(self, "_reader"): + self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 + return self._reader.read(bufsize) + else: + # receive bytes and convert to string + while True: + part: bytes = self._socket.recv(bufsize) + data: str = part.decode("utf-8") + return data class SocketManager: diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 7a75e6248844..7972eedd0919 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -128,22 +128,6 @@ def parse_rpc_message(data: str) -> Tuple[Dict[str, str], str]: print("json decode error") -def _listen_on_fifo(pipe_name: str, result: List[str], completed: threading.Event): - # Open the FIFO for reading - fifo_path = pathlib.Path(pipe_name) - with fifo_path.open() as fifo: - print("Waiting for data...") - while True: - if completed.is_set(): - break # Exit loop if completed event is set - data = fifo.read() # This will block until data is available - if len(data) == 0: - # If data is empty, assume EOF - break - print(f"Received: {data}") - result.append(data) - - def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event): """Listen on the named pipe or Unix domain socket for JSON data from the server. @@ -323,19 +307,14 @@ def runner_with_cwd_env( # if additional environment variables are passed, add them to the environment if env_add: env.update(env_add) - # server = UnixPipeServer(pipe_name) - # server.start() - ################# - # Create the FIFO (named pipe) if it doesn't exist - # if not pathlib.Path.exists(pipe_name): - os.mkfifo(pipe_name) - ################# + server = UnixPipeServer(pipe_name) + server.start() completed = threading.Event() result = [] # result is a string array to store the data during threading t1: threading.Thread = threading.Thread( - target=_listen_on_fifo, args=(pipe_name, result, completed) + target=_listen_on_pipe_new, args=(server, result, completed) ) t1.start() @@ -385,14 +364,14 @@ def generate_random_pipe_name(prefix=""): # For Windows, named pipes have a specific naming convention. if sys.platform == "win32": - return f"\\\\.\\pipe\\{prefix}-{random_suffix}" + return f"\\\\.\\pipe\\{prefix}-{random_suffix}-sock" # For Unix-like systems, use either the XDG_RUNTIME_DIR or a temporary directory. xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") if xdg_runtime_dir: - return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}") # noqa: PTH118 + return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}.sock") # noqa: PTH118 else: - return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}") # noqa: PTH118 + return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}.sock") # noqa: PTH118 class UnixPipeServer: diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index cba3a2d1f59d..09e61ff40518 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -18,6 +18,8 @@ from typing_extensions import NotRequired # noqa: E402 +from testing_tools import socket_manager # noqa: E402 + # Types @@ -329,10 +331,10 @@ def send_post_request( if __writer is None: try: - __writer = open(test_run_pipe, "w", encoding="utf-8", newline="\r\n") # noqa: SIM115, PTH123 + __writer = socket_manager.PipeManager(test_run_pipe) + __writer.connect() except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {test_run_pipe}[vscode-unittest]: {error}" - print(error_msg, file=sys.stderr) __writer = None raise VSCodeUnittestError(error_msg) from error @@ -341,11 +343,10 @@ def send_post_request( "params": payload, } data = json.dumps(rpc) + try: if __writer: - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - __writer.write(request) - __writer.flush() + __writer.write(data) else: print( f"Connection error[vscode-unittest], writer is None \n[vscode-unittest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 561f6e432341..8a54a7249d71 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -21,6 +21,11 @@ import pytest +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import socket_manager # noqa: E402 + if TYPE_CHECKING: from pluggy import Result @@ -166,7 +171,7 @@ def pytest_exception_interact(node, call, report): collected_test = TestRunResultDict() collected_test[node_id] = item_result cwd = pathlib.Path.cwd() - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -290,7 +295,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -324,7 +329,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -400,7 +405,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + post_response(os.fsdecode(cwd), error_node) try: session_node: TestNode | None = build_test_tree(session) if not session_node: @@ -408,7 +413,7 @@ def pytest_sessionfinish(session, exitstatus): "Something went wrong following pytest finish, \ no session node was created" ) - send_discovery_message(os.fsdecode(cwd), session_node) + post_response(os.fsdecode(cwd), session_node) except Exception as e: ERRORS.append( f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" @@ -420,7 +425,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + post_response(os.fsdecode(cwd), error_node) else: if exitstatus == 0 or exitstatus == 1: exitstatus_bool = "success" @@ -430,7 +435,7 @@ def pytest_sessionfinish(session, exitstatus): ) exitstatus_bool = "error" - send_execution_message( + execution_post( os.fsdecode(cwd), exitstatus_bool, None, @@ -484,7 +489,7 @@ def pytest_sessionfinish(session, exitstatus): result=file_coverage_map, error=None, ) - send_message(payload) + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -852,10 +857,8 @@ def get_node_path(node: Any) -> pathlib.Path: atexit.register(lambda: __writer.close() if __writer else None) -def send_execution_message( - cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None -): - """Sends message execution payload details. +def execution_post(cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None): + """Sends a POST request with execution payload details. Args: cwd (str): Current working directory. @@ -867,10 +870,10 @@ def send_execution_message( ) if ERRORS: payload["error"] = ERRORS - send_message(payload) + send_post_request(payload) -def send_discovery_message(cwd: str, session_node: TestNode) -> None: +def post_response(cwd: str, session_node: TestNode) -> None: """ Sends a POST request with test session details in payload. @@ -886,7 +889,7 @@ def send_discovery_message(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - send_message(payload, cls_encoder=PathEncoder) + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -898,7 +901,7 @@ def default(self, o): return super().default(o) -def send_message( +def send_post_request( payload: ExecutionPayloadDict | DiscoveryPayloadDict | CoveragePayloadDict, cls_encoder=None, ): @@ -923,7 +926,8 @@ def send_message( if __writer is None: try: - __writer = open(TEST_RUN_PIPE, "w", encoding="utf-8", newline="\r\n") # noqa: SIM115, PTH123 + __writer = socket_manager.PipeManager(TEST_RUN_PIPE) + __writer.connect() except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {TEST_RUN_PIPE}[vscode-pytest]: {error}" print(error_msg, file=sys.stderr) @@ -941,11 +945,10 @@ def send_message( "params": payload, } data = json.dumps(rpc, cls=cls_encoder) + try: if __writer: - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - __writer.write(request) - __writer.flush() + __writer.write(data) else: print( f"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/_common.py b/python_files/vscode_pytest/_common.py deleted file mode 100644 index 9f835f555b6e..000000000000 --- a/python_files/vscode_pytest/_common.py +++ /dev/null @@ -1,2 +0,0 @@ -# def send_post_request(): -# return diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts index 8cccd4cdcfed..c6010d491822 100644 --- a/src/client/common/pipes/namedPipes.ts +++ b/src/client/common/pipes/namedPipes.ts @@ -1,18 +1,67 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as cp from 'child_process'; import * as crypto from 'crypto'; -import * as fs from 'fs-extra'; import * as net from 'net'; import * as os from 'os'; import * as path from 'path'; import * as rpc from 'vscode-jsonrpc/node'; -import { CancellationError, CancellationToken, Disposable } from 'vscode'; import { traceVerbose } from '../../logging'; -import { isWindows } from '../utils/platform'; -import { createDeferred } from '../utils/async'; -import { noop } from '../utils/misc'; + +export interface ConnectedServerObj { + serverOnClosePromise(): Promise; +} + +export function createNamedPipeServer( + pipeName: string, + onConnectionCallback: (value: [rpc.MessageReader, rpc.MessageWriter]) => void, +): Promise { + traceVerbose(`Creating named pipe server on ${pipeName}`); + + let connectionCount = 0; + return new Promise((resolve, reject) => { + // create a server, resolves and returns server on listen + const server = net.createServer((socket) => { + // this lambda function is called whenever a client connects to the server + connectionCount += 1; + traceVerbose('new client is connected to the socket, connectionCount: ', connectionCount, pipeName); + socket.on('close', () => { + // close event is emitted by client to the server + connectionCount -= 1; + traceVerbose('client emitted close event, connectionCount: ', connectionCount); + if (connectionCount <= 0) { + // if all clients are closed, close the server + traceVerbose('connection count is <= 0, closing the server: ', pipeName); + server.close(); + } + }); + + // upon connection create a reader and writer and pass it to the callback + onConnectionCallback([ + new rpc.SocketMessageReader(socket, 'utf-8'), + new rpc.SocketMessageWriter(socket, 'utf-8'), + ]); + }); + const closedServerPromise = new Promise((resolveOnServerClose) => { + // get executed on connection close and resolves + // implementation of the promise is the arrow function + server.on('close', resolveOnServerClose); + }); + server.on('error', reject); + + server.listen(pipeName, () => { + // this function is called when the server is listening + server.removeListener('error', reject); + const connectedServer = { + // when onClosed event is called, so is closed function + // goes backwards up the chain, when resolve2 is called, so is onClosed that means server.onClosed() on the other end can work + // event C + serverOnClosePromise: () => closedServerPromise, + }; + resolve(connectedServer); + }); + }); +} const { XDG_RUNTIME_DIR } = process.env; export function generateRandomPipeName(prefix: string): string { @@ -23,178 +72,20 @@ export function generateRandomPipeName(prefix: string): string { } if (process.platform === 'win32') { - return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; + return `\\\\.\\pipe\\${prefix}-${randomSuffix}-sock`; } let result; if (XDG_RUNTIME_DIR) { - result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}.sock`); } else { - result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}.sock`); } return result; } -async function mkfifo(fifoPath: string): Promise { - return new Promise((resolve, reject) => { - const proc = cp.spawn('mkfifo', [fifoPath]); - proc.on('error', (err) => { - reject(err); - }); - proc.on('exit', (code) => { - if (code === 0) { - resolve(); - } - }); - }); -} - -export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { - // windows implementation of FIFO using named pipes - if (isWindows()) { - const deferred = createDeferred(); - const server = net.createServer((socket) => { - traceVerbose(`Pipe connected: ${pipeName}`); - server.close(); - deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); - }); - - server.on('error', deferred.reject); - server.listen(pipeName); - if (token) { - token.onCancellationRequested(() => { - if (server.listening) { - server.close(); - } - deferred.reject(new CancellationError()); - }); - } - return deferred.promise; - } - // linux implementation of FIFO - await mkfifo(pipeName); - try { - await fs.chmod(pipeName, 0o666); - } catch { - // Intentionally ignored - } - const writer = fs.createWriteStream(pipeName, { - encoding: 'utf-8', - }); - return new rpc.StreamMessageWriter(writer, 'utf-8'); -} - -class CombinedReader implements rpc.MessageReader { - private _onError = new rpc.Emitter(); - - private _onClose = new rpc.Emitter(); - - private _onPartialMessage = new rpc.Emitter(); - - // eslint-disable-next-line @typescript-eslint/no-empty-function - private _callback: rpc.DataCallback = () => {}; - - private _disposables: rpc.Disposable[] = []; - - private _readers: rpc.MessageReader[] = []; - - constructor() { - this._disposables.push(this._onClose, this._onError, this._onPartialMessage); - } - - onError: rpc.Event = this._onError.event; - - onClose: rpc.Event = this._onClose.event; - - onPartialMessage: rpc.Event = this._onPartialMessage.event; - - listen(callback: rpc.DataCallback): rpc.Disposable { - this._callback = callback; - // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function - return new Disposable(() => (this._callback = () => {})); - } - - add(reader: rpc.MessageReader): void { - this._readers.push(reader); - reader.listen((msg) => { - this._callback(msg as rpc.NotificationMessage); - }); - this._disposables.push(reader); - reader.onClose(() => { - this.remove(reader); - if (this._readers.length === 0) { - this._onClose.fire(); - } - }); - reader.onError((e) => { - this.remove(reader); - this._onError.fire(e); - }); - } - - remove(reader: rpc.MessageReader): void { - const found = this._readers.find((r) => r === reader); - if (found) { - this._readers = this._readers.filter((r) => r !== reader); - reader.dispose(); - } - } - - dispose(): void { - this._readers.forEach((r) => r.dispose()); - this._readers = []; - this._disposables.forEach((disposable) => disposable.dispose()); - this._disposables = []; - } -} - -export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { - if (isWindows()) { - // windows implementation of FIFO using named pipes - const deferred = createDeferred(); - const combined = new CombinedReader(); - - let refs = 0; - const server = net.createServer((socket) => { - traceVerbose(`Pipe connected: ${pipeName}`); - refs += 1; - - socket.on('close', () => { - refs -= 1; - if (refs <= 0) { - server.close(); - } - }); - combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); - }); - server.on('error', deferred.reject); - server.listen(pipeName); - if (token) { - token.onCancellationRequested(() => { - if (server.listening) { - server.close(); - } - deferred.reject(new CancellationError()); - }); - } - deferred.resolve(combined); - return deferred.promise; - } - // mac/linux implementation of FIFO - await mkfifo(pipeName); - try { - await fs.chmod(pipeName, 0o666); - } catch { - // Intentionally ignored - } - const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); - const socket = new net.Socket({ fd }); - const reader = new rpc.SocketMessageReader(socket, 'utf-8'); - socket.on('close', () => { - fs.close(fd).catch(noop); - reader.dispose(); - }); - - return reader; +export function namedPipeClient(name: string): [rpc.MessageReader, rpc.MessageWriter] { + const socket = net.connect(name); + return [new rpc.SocketMessageReader(socket, 'utf-8'), new rpc.SocketMessageWriter(socket, 'utf-8')]; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index b6848d0245dc..1d2be64f537d 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -16,7 +16,7 @@ import { ITestResultResolver, } from './types'; import { Deferred, createDeferred } from '../../../common/utils/async'; -import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { createNamedPipeServer, generateRandomPipeName } from '../../../common/pipes/namedPipes'; import { EXTENSION_ROOT_DIR } from '../../../constants'; export function fixLogLinesNoTrailing(content: string): string { @@ -34,6 +34,28 @@ export function createTestingDeferred(): Deferred { return createDeferred(); } +export async function startTestIdsNamedPipe(testIds: string[]): Promise { + const pipeName: string = generateRandomPipeName('python-test-ids'); + // uses callback so the on connect action occurs after the pipe is created + await createNamedPipeServer(pipeName, ([_reader, writer]) => { + traceVerbose('Test Ids named pipe connected'); + // const num = await + const msg = { + jsonrpc: '2.0', + params: testIds, + } as Message; + writer + .write(msg) + .then(() => { + writer.end(); + }) + .catch((ex) => { + traceError('Failed to write test ids to named pipe', ex); + }); + }); + return pipeName; +} + interface ExecutionResultMessage extends Message { params: ExecutionTestPayload; } @@ -72,47 +94,47 @@ export async function startRunResultNamedPipe( dataReceivedCallback: (payload: ExecutionTestPayload) => void, deferredTillServerClose: Deferred, cancellationToken?: CancellationToken, -): Promise { +): Promise<{ name: string } & Disposable> { traceVerbose('Starting Test Result named pipe'); const pipeName: string = generateRandomPipeName('python-test-results'); - - const reader = await createReaderPipe(pipeName, cancellationToken); - traceVerbose(`Test Results named pipe ${pipeName} connected`); - let disposables: Disposable[] = []; - const disposable = new Disposable(() => { - traceVerbose(`Test Results named pipe ${pipeName} disposed`); - disposables.forEach((d) => d.dispose()); - disposables = []; + let disposeOfServer: () => void = () => { deferredTillServerClose.resolve(); - }); + /* noop */ + }; + const server = await createNamedPipeServer(pipeName, ([reader, _writer]) => { + // this lambda function is: onConnectionCallback + // this is called once per client connecting to the server + traceVerbose(`Test Result named pipe ${pipeName} connected`); + let perConnectionDisposables: (Disposable | undefined)[] = [reader]; - if (cancellationToken) { - disposables.push( + // create a function to dispose of the server + disposeOfServer = () => { + // dispose of all data listeners and cancelation listeners + perConnectionDisposables.forEach((d) => d?.dispose()); + perConnectionDisposables = []; + deferredTillServerClose.resolve(); + }; + perConnectionDisposables.push( + // per connection, add a listener for the cancellation token and the data cancellationToken?.onCancellationRequested(() => { - traceLog(`Test Result named pipe ${pipeName} cancelled`); - disposable.dispose(); + console.log(`Test Result named pipe ${pipeName} cancelled`); + // if cancel is called on one connection, dispose of all connections + disposeOfServer(); + }), + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); }), ); - } - disposables.push( - reader, - reader.listen((data: Message) => { - traceVerbose(`Test Result named pipe ${pipeName} received data`); - // if EOT, call decrement connection count (callback) - dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); - }), - reader.onClose(() => { + server.serverOnClosePromise().then(() => { // this is called once the server close, once per run instance traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); // dispose of all data listeners and cancelation listeners - disposable.dispose(); - }), - reader.onError((error) => { - traceError(`Test Results named pipe ${pipeName} error:`, error); - }), - ); + disposeOfServer(); + }); + }); - return pipeName; + return { name: pipeName, dispose: disposeOfServer }; } interface DiscoveryResultMessage extends Message { @@ -122,44 +144,36 @@ interface DiscoveryResultMessage extends Message { export async function startDiscoveryNamedPipe( callback: (payload: DiscoveredTestPayload) => void, cancellationToken?: CancellationToken, -): Promise { +): Promise<{ name: string } & Disposable> { traceVerbose('Starting Test Discovery named pipe'); - // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; const pipeName: string = generateRandomPipeName('python-test-discovery'); - const reader = await createReaderPipe(pipeName, cancellationToken); - - traceVerbose(`Test Discovery named pipe ${pipeName} connected`); - let disposables: Disposable[] = []; - const disposable = new Disposable(() => { - traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); - disposables.forEach((d) => d.dispose()); - disposables = []; - }); - - if (cancellationToken) { + let dispose: () => void = () => { + /* noop */ + }; + await createNamedPipeServer(pipeName, ([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(() => { + cancellationToken?.onCancellationRequested(() => { traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); - disposable.dispose(); + dispose(); + }), + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + dispose(); }), ); - } - - disposables.push( - reader, - reader.listen((data: Message) => { - traceVerbose(`Test Discovery named pipe ${pipeName} received data`); - callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); - }), - reader.onClose(() => { - traceVerbose(`Test Discovery named pipe ${pipeName} closed`); - disposable.dispose(); - }), - reader.onError((error) => { - traceError(`Test Discovery named pipe ${pipeName} error:`, error); - }), - ); - return pipeName; + }); + return { name: pipeName, dispose }; } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index ff73b31435a3..4baa76000d14 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -42,12 +42,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { executionFactory?: IPythonExecutionFactory, interpreter?: PythonEnvironment, ): Promise { - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); - await this.runPytestDiscovery(uri, name, executionFactory, interpreter); - + try { + await this.runPytestDiscovery(uri, name, executionFactory, interpreter); + } finally { + dispose(); + } // this is only a placeholder to handle function overloading until rewrite is finished const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; return discoveryPayload; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b408280a576e..b108bd876e5a 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import * as path from 'path'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; @@ -49,16 +49,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); - - const name = await utils.startRunResultNamedPipe( + const { name, dispose: serverDispose } = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - cSource.token, // token to cancel + runInstance?.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); + // if canceled, stop listening for results + serverDispose(); // this will resolve deferredTillServerClose + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', @@ -72,7 +72,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri, testIds, name, - cSource, + serverDispose, runInstance, profileKind, executionFactory, @@ -97,7 +97,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverCancel: CancellationTokenSource, + serverDispose: () => void, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -174,7 +174,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher!.launchDebugger( launchOptions, () => { - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }, sessionOptions, ); @@ -196,7 +196,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -216,7 +216,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); await deferredTillExecClose.promise; } else { @@ -239,7 +239,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose.resolve(); - serverCancel.cancel(); } }); @@ -283,12 +282,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } + // this doesn't work, it instead directs us to the noop one which is defined first + // potentially this is due to the server already being close, if this is the case? + serverDispose(); // this will resolve deferredTillServerClose } - - // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs // due to the sync reading of the output. deferredTillExecClose.resolve(); - serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 04518e121651..c04ec4d54b45 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -45,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); @@ -67,7 +67,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { try { await this.runDiscovery(uri, options, name, cwd, executionFactory); } finally { - // none + dispose(); } // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 6db36d96149f..350709392570 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; -import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; @@ -59,24 +59,23 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); - const name = await utils.startRunResultNamedPipe( + const { name: resultNamedPipeName, dispose: serverDispose } = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - cSource.token, // token to cancel + runInstance?.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); // if canceled, stop listening for results deferredTillServerClose.resolve(); + serverDispose(); }); try { await this.runTestsNew( uri, testIds, - name, - cSource, + resultNamedPipeName, + serverDispose, runInstance, profileKind, executionFactory, @@ -99,7 +98,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverCancel: CancellationTokenSource, + serverDispose: () => void, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -179,7 +178,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher.launchDebugger( launchOptions, () => { - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }, sessionOptions, ); @@ -198,7 +197,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -218,7 +217,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); await deferredTillExecClose.promise; } else { @@ -239,7 +238,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose?.resolve(); - serverCancel.cancel(); } }); @@ -276,9 +274,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } + serverDispose(); } deferredTillExecClose.resolve(); - serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 24a34f8645ed..8a1891962429 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -81,6 +81,8 @@ suite('End to End Tests: test adapters', () => { 'coverageWorkspace', ); suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + // create symlink for specific symlink test const target = rootPathSmallWorkspace; const dest = rootPathDiscoverySymlink; @@ -105,7 +107,6 @@ suite('End to End Tests: test adapters', () => { }); setup(async () => { - serviceContainer = (await initialize()).serviceContainer; getPixiStub = sinon.stub(pixi, 'getPixi'); getPixiStub.resolves(undefined); @@ -677,7 +678,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -874,7 +875,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -1061,12 +1062,88 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); + test('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; + } + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + if (data.result === undefined) { + failureOccurred = true; + failureMsg = 'Expected results to be present'; + } + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected testId to be present'; + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + configService, + testOutputChannel.object, + resultResolver, + envVarsService, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); test('pytest execution adapter seg fault error handling', async () => { resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; - console.log('EFB: beginning function'); resultResolver._resolveExecution = async (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); @@ -1092,7 +1169,7 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); + // return Promise.resolve(); }; const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 538b77161483..18680b123b21 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -45,7 +45,14 @@ suite('pytest test discovery adapter', () => { mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + utilsStartDiscoveryNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'discoveryResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); // constants expectedPath = path.join('/', 'my', 'test', 'path'); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 18cabcc96772..b3f226402b72 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -88,7 +88,14 @@ suite('pytest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + utilsStartRunResultNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'runResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 81480d08b2b8..d8b685b13654 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -71,9 +71,6 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; - // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -100,13 +97,11 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); - utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { deferredTillServerCloseTester = deferredTillServerClose; - token?.onCancellationRequested(() => { - deferredTillServerCloseTester?.resolve(); - }); - - return Promise.resolve('named-pipes-socket-name'); + return Promise.resolve({ name: 'named-pipes-socket-name', dispose: serverDisposeStub }); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -132,6 +127,9 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; + + // assert the server dispose function was called correctly + sinon.assert.calledOnce(serverDisposeStub); }); test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { // mock test run and cancelation token @@ -140,9 +138,6 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; - // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -169,12 +164,14 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { deferredTillServerCloseTester = deferredTillServerClose; - token?.onCancellationRequested(() => { - deferredTillServerCloseTester?.resolve(); + return Promise.resolve({ + name: 'named-pipes-socket-name', + dispose: serverDisposeStub, }); - return Promise.resolve('named-pipes-socket-name'); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -213,6 +210,10 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; + + // TODO: fix the server disposal so it is called once not twice, + // currently not a problem but would be useful to improve clarity + sinon.assert.called(serverDisposeStub); }); }); }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index a0ee65d57922..4c92c8c0b682 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -82,7 +82,14 @@ suite('Unittest test discovery adapter', () => { }; utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + utilsStartDiscoveryNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'discoveryResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 78dcb0229e45..f2c06cd13496 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -87,7 +87,14 @@ suite('Unittest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + utilsStartRunResultNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'runResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py index 80be80f023c2..bad7ff8fcbbd 100644 --- a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -7,12 +7,11 @@ class TestSegmentationFault(unittest.TestCase): def cause_segfault(self): - print("Causing a segmentation fault") ctypes.string_at(0) # Dereference a NULL pointer def test_segfault(self): - self.cause_segfault() assert True + self.cause_segfault() if __name__ == "__main__":