Skip to content

Commit ef7c7e3

Browse files
switch to using temp file for test_ids (#24054)
first step in work on #23279 --------- Co-authored-by: Karthik Nadig <[email protected]>
1 parent e694910 commit ef7c7e3

File tree

9 files changed

+128
-153
lines changed

9 files changed

+128
-153
lines changed
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3-
4-
import sys
5-
6-
# Ignore the contents of this folder for Python 2 tests.
7-
if sys.version_info[0] < 3:
8-
collect_ignore_glob = ["*.py"]

python_files/unittestadapter/execution.py

Lines changed: 47 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import atexit
55
import enum
6-
import json
76
import os
87
import pathlib
98
import sys
@@ -24,7 +23,6 @@
2423

2524
from django_handler import django_execution_runner # noqa: E402
2625

27-
from testing_tools import process_json_util, socket_manager # noqa: E402
2826
from unittestadapter.pvsc_utils import ( # noqa: E402
2927
EOTPayloadDict,
3028
ExecutionPayloadDict,
@@ -269,8 +267,15 @@ def run_tests(
269267
return payload
270268

271269

270+
def execute_eot_and_cleanup():
271+
eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True}
272+
send_post_request(eot_payload, test_run_pipe)
273+
if __socket:
274+
__socket.close()
275+
276+
272277
__socket = None
273-
atexit.register(lambda: __socket.close() if __socket else None)
278+
atexit.register(execute_eot_and_cleanup)
274279

275280

276281
def send_run_data(raw_data, test_run_pipe):
@@ -306,70 +311,43 @@ def send_run_data(raw_data, test_run_pipe):
306311
if not test_run_pipe:
307312
print("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.")
308313
raise VSCodeUnittestError("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.")
309-
test_ids_from_buffer = []
310-
raw_json = None
311-
try:
312-
with socket_manager.PipeManager(run_test_ids_pipe) as sock:
313-
buffer: str = ""
314-
while True:
315-
# Receive the data from the client
316-
data: str = sock.read()
317-
if not data:
318-
break
319-
320-
# Append the received data to the buffer
321-
buffer += data
322-
323-
try:
324-
# Try to parse the buffer as JSON
325-
raw_json = process_json_util.process_rpc_json(buffer)
326-
# Clear the buffer as complete JSON object is received
327-
buffer = ""
328-
print("Received JSON data in run")
329-
break
330-
except json.JSONDecodeError:
331-
# JSON decoding error, the complete JSON object is not yet received
332-
continue
333-
except OSError as e:
334-
msg = f"Error: Could not connect to RUN_TEST_IDS_PIPE: {e}"
335-
print(msg)
336-
raise VSCodeUnittestError(msg) from e
337-
314+
test_ids = []
338315
try:
339-
if raw_json and "params" in raw_json and raw_json["params"]:
340-
test_ids_from_buffer = raw_json["params"]
341-
# Check to see if we are running django tests.
342-
if manage_py_path := os.environ.get("MANAGE_PY_PATH"):
343-
args = argv[index + 1 :] or []
344-
django_execution_runner(manage_py_path, test_ids_from_buffer, args)
345-
# the django run subprocesses sends the eot payload.
346-
else:
347-
# Perform test execution.
348-
payload = run_tests(
349-
start_dir,
350-
test_ids_from_buffer,
351-
pattern,
352-
top_level_dir,
353-
verbosity,
354-
failfast,
355-
locals_,
356-
)
357-
eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True}
358-
send_post_request(eot_payload, test_run_pipe)
359-
else:
360-
# No test ids received from buffer
361-
cwd = os.path.abspath(start_dir) # noqa: PTH100
362-
status = TestExecutionStatus.error
363-
payload: ExecutionPayloadDict = {
364-
"cwd": cwd,
365-
"status": status,
366-
"error": "No test ids received from buffer",
367-
"result": None,
368-
}
369-
send_post_request(payload, test_run_pipe)
370-
eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True}
371-
send_post_request(eot_payload, test_run_pipe)
372-
except json.JSONDecodeError as exc:
373-
msg = "Error: Could not parse test ids from stdin"
374-
print(msg)
375-
raise VSCodeUnittestError(msg) from exc
316+
# Read the test ids from the file, attempt to delete file afterwords.
317+
ids_path = pathlib.Path(run_test_ids_pipe)
318+
test_ids = ids_path.read_text(encoding="utf-8").splitlines()
319+
print("Received test ids from temp file.")
320+
try:
321+
ids_path.unlink()
322+
except Exception as e:
323+
print("Error[vscode-pytest]: unable to delete temp file" + str(e))
324+
325+
except Exception as e:
326+
# No test ids received from buffer, return error payload
327+
cwd = pathlib.Path(start_dir).absolute()
328+
status: TestExecutionStatus = TestExecutionStatus.error
329+
payload: ExecutionPayloadDict = {
330+
"cwd": str(cwd),
331+
"status": status,
332+
"result": None,
333+
"error": "No test ids read from temp file," + str(e),
334+
}
335+
send_post_request(payload, test_run_pipe)
336+
337+
# If no error occurred, we will have test ids to run.
338+
if manage_py_path := os.environ.get("MANAGE_PY_PATH"):
339+
print("MANAGE_PY_PATH env var set, running Django test suite.")
340+
args = argv[index + 1 :] or []
341+
django_execution_runner(manage_py_path, test_ids, args)
342+
# the django run subprocesses sends the eot payload.
343+
else:
344+
# Perform regular unittest execution.
345+
payload = run_tests(
346+
start_dir,
347+
test_ids,
348+
pattern,
349+
top_level_dir,
350+
verbosity,
351+
failfast,
352+
locals_,
353+
)
Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
3-
import json
43
import os
54
import pathlib
65
import sys
@@ -17,10 +16,12 @@
1716
script_dir = pathlib.Path(__file__).parent.parent
1817
sys.path.append(os.fspath(script_dir))
1918
sys.path.append(os.fspath(script_dir / "lib" / "python"))
20-
from testing_tools import ( # noqa: E402
21-
process_json_util,
22-
socket_manager,
23-
)
19+
20+
21+
def run_pytest(args):
22+
arg_array = ["-p", "vscode_pytest", *args]
23+
pytest.main(arg_array)
24+
2425

2526
# This script handles running pytest via pytest.main(). It is called via run in the
2627
# pytest execution adapter and gets the test_ids to run via stdin and the rest of the
@@ -34,52 +35,21 @@
3435
# Get the rest of the args to run with pytest.
3536
args = sys.argv[1:]
3637
run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE")
37-
if not run_test_ids_pipe:
38-
print("Error[vscode-pytest]: RUN_TEST_IDS_PIPE env var is not set.")
39-
raw_json = {}
40-
try:
41-
socket_name = os.environ.get("RUN_TEST_IDS_PIPE")
42-
with socket_manager.PipeManager(socket_name) as sock:
43-
buffer = ""
44-
while True:
45-
# Receive the data from the client as a string
46-
data = sock.read(3000)
47-
if not data:
48-
break
49-
50-
# Append the received data to the buffer
51-
buffer += data
52-
53-
try:
54-
# Try to parse the buffer as JSON
55-
raw_json = process_json_util.process_rpc_json(buffer)
56-
# Clear the buffer as complete JSON object is received
57-
buffer = ""
58-
print("Received JSON data in run script")
59-
break
60-
except json.JSONDecodeError:
61-
# JSON decoding error, the complete JSON object is not yet received
62-
continue
63-
except UnicodeDecodeError:
64-
continue
65-
except OSError as e:
66-
print(f"Error: Could not connect to runTestIdsPort: {e}")
67-
print("Error: Could not connect to runTestIdsPort")
68-
try:
69-
test_ids_from_buffer = raw_json.get("params")
70-
if test_ids_from_buffer:
71-
arg_array = ["-p", "vscode_pytest", *args, *test_ids_from_buffer]
38+
if run_test_ids_pipe:
39+
try:
40+
# Read the test ids from the file, delete file, and run pytest.
41+
ids_path = pathlib.Path(run_test_ids_pipe)
42+
ids = ids_path.read_text(encoding="utf-8").splitlines()
43+
try:
44+
ids_path.unlink()
45+
except Exception as e:
46+
print("Error[vscode-pytest]: unable to delete temp file" + str(e))
47+
arg_array = ["-p", "vscode_pytest", *args, *ids]
7248
print("Running pytest with args: " + str(arg_array))
7349
pytest.main(arg_array)
74-
else:
75-
print(
76-
"Error: No test ids received from stdin, could be an error or a run request without ids provided.",
77-
)
78-
print("Running pytest with no test ids as args. Args being used: ", args)
79-
arg_array = ["-p", "vscode_pytest", *args]
80-
pytest.main(arg_array)
81-
except json.JSONDecodeError:
82-
print(
83-
"Error: Could not parse test ids from stdin. Raw json received from socket: \n",
84-
raw_json,
85-
)
50+
except Exception as e:
51+
print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e))
52+
run_pytest(args)
53+
else:
54+
print("Error[vscode-pytest]: RUN_TEST_IDS_PIPE env var is not set.")
55+
run_pytest(args)

src/client/testing/testController/common/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import * as net from 'net';
44
import * as path from 'path';
55
import * as fs from 'fs';
6+
import * as os from 'os';
7+
import * as crypto from 'crypto';
68
import { CancellationToken, Position, TestController, TestItem, Uri, Range, Disposable } from 'vscode';
79
import { Message } from 'vscode-jsonrpc';
810
import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging';
@@ -20,6 +22,7 @@ import {
2022
} from './types';
2123
import { Deferred, createDeferred } from '../../../common/utils/async';
2224
import { createNamedPipeServer, generateRandomPipeName } from '../../../common/pipes/namedPipes';
25+
import { EXTENSION_ROOT_DIR } from '../../../constants';
2326

2427
export function fixLogLines(content: string): string {
2528
const lines = content.split(/\r?\n/g);
@@ -193,6 +196,36 @@ interface ExecutionResultMessage extends Message {
193196
params: ExecutionTestPayload | EOTTestPayload;
194197
}
195198

199+
/**
200+
* Writes an array of test IDs to a temporary file.
201+
*
202+
* @param testIds - The array of test IDs to write.
203+
* @returns A promise that resolves to the file name of the temporary file.
204+
*/
205+
export async function writeTestIdsFile(testIds: string[]): Promise<string> {
206+
// temp file name in format of test-ids-<randomSuffix>.txt
207+
const randomSuffix = crypto.randomBytes(10).toString('hex');
208+
const tempName = `test-ids-${randomSuffix}.txt`;
209+
// create temp file
210+
let tempFileName: string;
211+
try {
212+
traceLog('Attempting to use temp directory for test ids file, file name:', tempName);
213+
tempFileName = path.join(os.tmpdir(), tempName);
214+
} catch (error) {
215+
// Handle the error when accessing the temp directory
216+
traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead');
217+
// Make new temp directory in extension root dir
218+
const tempDir = path.join(EXTENSION_ROOT_DIR, '.temp');
219+
await fs.promises.mkdir(tempDir, { recursive: true });
220+
tempFileName = path.join(EXTENSION_ROOT_DIR, '.temp', tempName);
221+
traceLog('New temp file:', tempFileName);
222+
}
223+
// write test ids to file
224+
await fs.promises.writeFile(tempFileName, testIds.join('\n'));
225+
// return file name
226+
return tempFileName;
227+
}
228+
196229
export async function startRunResultNamedPipe(
197230
dataReceivedCallback: (payload: ExecutionTestPayload | EOTTestPayload) => void,
198231
deferredTillServerClose: Deferred<void>,

src/client/testing/testController/pytest/pytestExecutionAdapter.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
142142
testArgs = utils.addValueIfKeyNotExist(testArgs, '--capture', 'no');
143143
}
144144

145-
// add port with run test ids to env vars
146-
const testIdsPipeName = await utils.startTestIdsNamedPipe(testIds);
147-
mutableEnv.RUN_TEST_IDS_PIPE = testIdsPipeName;
145+
// create a file with the test ids and set the environment variable to the file name
146+
const testIdsFileName = await utils.writeTestIdsFile(testIds);
147+
mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName;
148148
traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`);
149149

150150
const spawnOptions: SpawnOptions = {
@@ -162,7 +162,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
162162
args: testArgs,
163163
token: runInstance?.token,
164164
testProvider: PYTEST_PROVIDER,
165-
runTestIdsPort: testIdsPipeName,
165+
runTestIdsPort: testIdsFileName,
166166
pytestPort: resultNamedPipeName,
167167
};
168168
traceInfo(`Running DEBUG pytest with arguments: ${testArgs} for workspace ${uri.fsPath} \r\n`);

src/client/testing/testController/unittest/testExecutionAdapter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter {
137137
traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`);
138138

139139
// create named pipe server to send test ids
140-
const testIdsPipeName = await utils.startTestIdsNamedPipe(testIds);
141-
mutableEnv.RUN_TEST_IDS_PIPE = testIdsPipeName;
140+
const testIdsFileName = await utils.writeTestIdsFile(testIds);
141+
mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName;
142142
traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`);
143143

144144
const spawnOptions: SpawnOptions = {
@@ -167,7 +167,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter {
167167
args,
168168
token: options.token,
169169
testProvider: UNITTEST_PROVIDER,
170-
runTestIdsPort: testIdsPipeName,
170+
runTestIdsPort: testIdsFileName,
171171
pytestPort: resultNamedPipeName, // change this from pytest
172172
};
173173
traceInfo(`Running DEBUG unittest for workspace ${options.cwd} with arguments: ${args}\r\n`);

0 commit comments

Comments
 (0)