Skip to content

Commit be829b3

Browse files
authored
Unittest for large workspaces (#21351)
follows the same steps as making pytest compatible with large workspaces with many tests. Now test_ids are sent over a port as a json instead of in the exec function which can hit a cap on # of characters. Should fix #21339.
1 parent cd76ee1 commit be829b3

File tree

11 files changed

+285
-188
lines changed

11 files changed

+285
-188
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import io
4+
import json
5+
from typing import List
6+
7+
CONTENT_LENGTH: str = "Content-Length:"
8+
9+
10+
def process_rpc_json(data: str) -> List[str]:
11+
"""Process the JSON data which comes from the server."""
12+
str_stream: io.StringIO = io.StringIO(data)
13+
14+
length: int = 0
15+
16+
while True:
17+
line: str = str_stream.readline()
18+
if CONTENT_LENGTH.lower() in line.lower():
19+
length = int(line[len(CONTENT_LENGTH) :])
20+
break
21+
22+
if not line or line.isspace():
23+
raise ValueError("Header does not contain Content-Length")
24+
25+
while True:
26+
line: str = str_stream.readline()
27+
if not line or line.isspace():
28+
break
29+
30+
raw_json: str = str_stream.read(length)
31+
return json.loads(raw_json)

pythonFiles/tests/unittestadapter/test_execution.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,23 @@
2020
"111",
2121
"--uuid",
2222
"fake-uuid",
23-
"--testids",
24-
"test_file.test_class.test_method",
2523
],
26-
(111, "fake-uuid", ["test_file.test_class.test_method"]),
24+
(111, "fake-uuid"),
2725
),
2826
(
29-
["--port", "111", "--uuid", "fake-uuid", "--testids", ""],
30-
(111, "fake-uuid", [""]),
27+
["--port", "111", "--uuid", "fake-uuid"],
28+
(111, "fake-uuid"),
3129
),
3230
(
3331
[
3432
"--port",
3533
"111",
3634
"--uuid",
3735
"fake-uuid",
38-
"--testids",
39-
"test_file.test_class.test_method",
4036
"-v",
4137
"-s",
4238
],
43-
(111, "fake-uuid", ["test_file.test_class.test_method"]),
39+
(111, "fake-uuid"),
4440
),
4541
],
4642
)

pythonFiles/unittestadapter/execution.py

+64-7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
import enum
66
import json
77
import os
8+
import pathlib
9+
import socket
810
import sys
911
import traceback
1012
import unittest
1113
from types import TracebackType
1214
from typing import Dict, List, Optional, Tuple, Type, Union
1315

16+
script_dir = pathlib.Path(__file__).parent.parent
17+
sys.path.append(os.fspath(script_dir))
18+
sys.path.append(os.fspath(script_dir / "lib" / "python"))
19+
from testing_tools import process_json_util
20+
1421
# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager.
1522
PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1623
sys.path.insert(0, PYTHON_FILES)
@@ -25,7 +32,7 @@
2532

2633
def parse_execution_cli_args(
2734
args: List[str],
28-
) -> Tuple[int, Union[str, None], List[str]]:
35+
) -> Tuple[int, Union[str, None]]:
2936
"""Parse command-line arguments that should be processed by the script.
3037
3138
So far this includes the port number that it needs to connect to, the uuid passed by the TS side,
@@ -39,10 +46,9 @@ def parse_execution_cli_args(
3946
arg_parser = argparse.ArgumentParser()
4047
arg_parser.add_argument("--port", default=DEFAULT_PORT)
4148
arg_parser.add_argument("--uuid")
42-
arg_parser.add_argument("--testids", nargs="+")
4349
parsed_args, _ = arg_parser.parse_known_args(args)
4450

45-
return (int(parsed_args.port), parsed_args.uuid, parsed_args.testids)
51+
return (int(parsed_args.port), parsed_args.uuid)
4652

4753

4854
ErrorType = Union[
@@ -226,11 +232,62 @@ def run_tests(
226232

227233
start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :])
228234

229-
# Perform test execution.
230-
port, uuid, testids = parse_execution_cli_args(argv[:index])
231-
payload = run_tests(start_dir, testids, pattern, top_level_dir, uuid)
235+
run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT")
236+
run_test_ids_port_int = (
237+
int(run_test_ids_port) if run_test_ids_port is not None else 0
238+
)
239+
240+
# get data from socket
241+
test_ids_from_buffer = []
242+
try:
243+
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
244+
client_socket.connect(("localhost", run_test_ids_port_int))
245+
print(f"CLIENT: Server listening on port {run_test_ids_port_int}...")
246+
buffer = b""
247+
248+
while True:
249+
# Receive the data from the client
250+
data = client_socket.recv(1024 * 1024)
251+
if not data:
252+
break
253+
254+
# Append the received data to the buffer
255+
buffer += data
256+
257+
try:
258+
# Try to parse the buffer as JSON
259+
test_ids_from_buffer = process_json_util.process_rpc_json(
260+
buffer.decode("utf-8")
261+
)
262+
# Clear the buffer as complete JSON object is received
263+
buffer = b""
264+
265+
# Process the JSON data
266+
print(f"Received JSON data: {test_ids_from_buffer}")
267+
break
268+
except json.JSONDecodeError:
269+
# JSON decoding error, the complete JSON object is not yet received
270+
continue
271+
except socket.error as e:
272+
print(f"Error: Could not connect to runTestIdsPort: {e}")
273+
print("Error: Could not connect to runTestIdsPort")
274+
275+
port, uuid = parse_execution_cli_args(argv[:index])
276+
if test_ids_from_buffer:
277+
# Perform test execution.
278+
payload = run_tests(
279+
start_dir, test_ids_from_buffer, pattern, top_level_dir, uuid
280+
)
281+
else:
282+
cwd = os.path.abspath(start_dir)
283+
status = TestExecutionStatus.error
284+
payload: PayloadDict = {
285+
"cwd": cwd,
286+
"status": status,
287+
"error": "No test ids received from buffer",
288+
}
232289

233-
# Build the request data (it has to be a POST request or the Node side will not process it), and send it.
290+
# Build the request data and send it.
234291
addr = ("localhost", port)
235292
data = json.dumps(payload)
236293
request = f"""Content-Length: {len(data)}

pythonFiles/vscode_pytest/run_pytest_script.py

+7-29
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
3-
import io
43
import json
54
import os
65
import pathlib
76
import socket
87
import sys
9-
from typing import List
108

119
import pytest
1210

13-
CONTENT_LENGTH: str = "Content-Length:"
14-
15-
16-
def process_rpc_json(data: str) -> List[str]:
17-
"""Process the JSON data which comes from the server which runs the pytest discovery."""
18-
str_stream: io.StringIO = io.StringIO(data)
19-
20-
length: int = 0
21-
22-
while True:
23-
line: str = str_stream.readline()
24-
if CONTENT_LENGTH.lower() in line.lower():
25-
length = int(line[len(CONTENT_LENGTH) :])
26-
break
27-
28-
if not line or line.isspace():
29-
raise ValueError("Header does not contain Content-Length")
30-
31-
while True:
32-
line: str = str_stream.readline()
33-
if not line or line.isspace():
34-
break
35-
36-
raw_json: str = str_stream.read(length)
37-
return json.loads(raw_json)
38-
11+
script_dir = pathlib.Path(__file__).parent.parent
12+
sys.path.append(os.fspath(script_dir))
13+
sys.path.append(os.fspath(script_dir / "lib" / "python"))
14+
from testing_tools import process_json_util
3915

4016
# This script handles running pytest via pytest.main(). It is called via run in the
4117
# pytest execution adapter and gets the test_ids to run via stdin and the rest of the
@@ -69,7 +45,9 @@ def process_rpc_json(data: str) -> List[str]:
6945

7046
try:
7147
# Try to parse the buffer as JSON
72-
test_ids_from_buffer = process_rpc_json(buffer.decode("utf-8"))
48+
test_ids_from_buffer = process_json_util.process_rpc_json(
49+
buffer.decode("utf-8")
50+
)
7351
# Clear the buffer as complete JSON object is received
7452
buffer = b""
7553

src/client/testing/common/debugLauncher.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,20 @@ export class DebugLauncher implements ITestDebugLauncher {
202202
throw Error(`Invalid debug config "${debugConfig.name}"`);
203203
}
204204
launchArgs.request = 'launch';
205+
206+
// Both types of tests need to have the port for the test result server.
207+
if (options.runTestIdsPort) {
208+
launchArgs.env = {
209+
...launchArgs.env,
210+
RUN_TEST_IDS_PORT: options.runTestIdsPort,
211+
};
212+
}
205213
if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) {
206214
if (options.pytestPort && options.pytestUUID) {
207215
launchArgs.env = {
208216
...launchArgs.env,
209217
TEST_PORT: options.pytestPort,
210218
TEST_UUID: options.pytestUUID,
211-
RUN_TEST_IDS_PORT: options.pytestRunTestIdsPort,
212219
};
213220
} else {
214221
throw Error(

src/client/testing/common/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type LaunchOptions = {
2727
outChannel?: OutputChannel;
2828
pytestPort?: string;
2929
pytestUUID?: string;
30-
pytestRunTestIdsPort?: string;
30+
runTestIdsPort?: string;
3131
};
3232

3333
export type ParserOptions = TestDiscoveryOptions;

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

+7-19
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,18 @@ export class PythonTestServer implements ITestServer, Disposable {
106106
return this._onDataReceived.event;
107107
}
108108

109-
async sendCommand(options: TestCommandOptions): Promise<void> {
109+
async sendCommand(options: TestCommandOptions, runTestIdPort?: string): Promise<void> {
110110
const { uuid } = options;
111111
const spawnOptions: SpawnOptions = {
112112
token: options.token,
113113
cwd: options.cwd,
114114
throwOnStdErr: true,
115115
outputChannel: options.outChannel,
116+
extraVariables: {},
116117
};
117118

119+
if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort;
118120
const isRun = !options.testIds;
119-
120121
// Create the Python environment in which to execute the command.
121122
const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = {
122123
allowEnvironmentFetchExceptions: false,
@@ -127,23 +128,9 @@ export class PythonTestServer implements ITestServer, Disposable {
127128
// Add the generated UUID to the data to be sent (expecting to receive it back).
128129
// first check if we have testIds passed in (in case of execution) and
129130
// insert appropriate flag and test id array
130-
let args = [];
131-
if (options.testIds) {
132-
args = [
133-
options.command.script,
134-
'--port',
135-
this.getPort().toString(),
136-
'--uuid',
137-
uuid,
138-
'--testids',
139-
...options.testIds,
140-
].concat(options.command.args);
141-
} else {
142-
// if not case of execution, go with the normal args
143-
args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat(
144-
options.command.args,
145-
);
146-
}
131+
const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat(
132+
options.command.args,
133+
);
147134

148135
if (options.outChannel) {
149136
options.outChannel.appendLine(`python ${args.join(' ')}`);
@@ -156,6 +143,7 @@ export class PythonTestServer implements ITestServer, Disposable {
156143
args,
157144
token: options.token,
158145
testProvider: UNITTEST_PROVIDER,
146+
runTestIdsPort: runTestIdPort,
159147
};
160148
traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`);
161149
await this.debugLauncher!.launchDebugger(launchOptions);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export type TestCommandOptionsPytest = {
172172
*/
173173
export interface ITestServer {
174174
readonly onDataReceived: Event<DataReceivedEvent>;
175-
sendCommand(options: TestCommandOptions): Promise<void>;
175+
sendCommand(options: TestCommandOptions, runTestIdsPort?: string): Promise<void>;
176176
serverReady(): Promise<void>;
177177
getPort(): number;
178178
createUUID(cwd: string): string;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
168168
testProvider: PYTEST_PROVIDER,
169169
pytestPort,
170170
pytestUUID,
171-
pytestRunTestIdsPort,
171+
runTestIdsPort: pytestRunTestIdsPort,
172172
};
173173
traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`);
174174
await debugLauncher!.launchDebugger(launchOptions);

0 commit comments

Comments
 (0)