Skip to content

Commit f0a4a4e

Browse files
switch to using tcp for comm with server (#20981)
code written by @karthiknadig and @eleanorjboyd, which switches to using TCP as the communication channel between the test adapter in the extension and the node server that handles the discovery/running of python tests. --------- Co-authored-by: Karthik Nadig <[email protected]>
1 parent 85e5b3e commit f0a4a4e

File tree

5 files changed

+126
-158
lines changed

5 files changed

+126
-158
lines changed

pythonFiles/unittestadapter/discovery.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,16 @@ def discover_tests(
121121

122122
# Build the request data (it has to be a POST request or the Node side will not process it), and send it.
123123
addr = ("localhost", port)
124-
with socket_manager.SocketManager(addr) as s:
125-
data = json.dumps(payload)
126-
request = f"""POST / HTTP/1.1
127-
Host: localhost:{port}
128-
Content-Length: {len(data)}
124+
data = json.dumps(payload)
125+
request = f"""Content-Length: {len(data)}
129126
Content-Type: application/json
130127
Request-uuid: {uuid}
131128
132129
{data}"""
133-
result = s.socket.sendall(request.encode("utf-8")) # type: ignore
130+
try:
131+
with socket_manager.SocketManager(addr) as s:
132+
if s.socket is not None:
133+
s.socket.sendall(request.encode("utf-8"))
134+
except Exception as e:
135+
print(f"Error sending response: {e}")
136+
print(f"Request data: {request}")

pythonFiles/unittestadapter/execution.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,16 @@ def run_tests(
222222

223223
# Build the request data (it has to be a POST request or the Node side will not process it), and send it.
224224
addr = ("localhost", port)
225-
with socket_manager.SocketManager(addr) as s:
226-
data = json.dumps(payload)
227-
request = f"""POST / HTTP/1.1
228-
Host: localhost:{port}
229-
Content-Length: {len(data)}
225+
data = json.dumps(payload)
226+
request = f"""Content-Length: {len(data)}
230227
Content-Type: application/json
231228
Request-uuid: {uuid}
232229
233230
{data}"""
234-
result = s.socket.sendall(request.encode("utf-8")) # type: ignore
231+
try:
232+
with socket_manager.SocketManager(addr) as s:
233+
if s.socket is not None:
234+
s.socket.sendall(request.encode("utf-8"))
235+
except Exception as e:
236+
print(f"Error sending response: {e}")
237+
print(f"Request data: {request}")

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ export function fixLogLines(content: string): string {
55
const lines = content.split(/\r?\n/g);
66
return `${lines.join('\r\n')}\r\n`;
77
}
8-
export interface IJSONRPCMessage {
8+
export interface IJSONRPCContent {
9+
extractedJSON: string;
10+
remainingRawData: string;
11+
}
12+
13+
export interface IJSONRPCHeaders {
914
headers: Map<string, string>;
10-
extractedData: string;
1115
remainingRawData: string;
1216
}
1317

14-
export function jsonRPCProcessor(rawData: string): IJSONRPCMessage {
18+
export const JSONRPC_UUID_HEADER = 'Request-uuid';
19+
export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length';
20+
export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type';
21+
22+
export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders {
1523
const lines = rawData.split('\n');
1624
let remainingRawData = '';
1725
const headerMap = new Map<string, string>();
@@ -22,17 +30,23 @@ export function jsonRPCProcessor(rawData: string): IJSONRPCMessage {
2230
break;
2331
}
2432
const [key, value] = line.split(':');
25-
if (['Content-Length', 'Content-Type', 'Request-uuid'].includes(key)) {
33+
if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) {
2634
headerMap.set(key.trim(), value.trim());
2735
}
2836
}
2937

30-
const length = parseInt(headerMap.get('Content-Length') ?? '0', 10);
31-
const data = remainingRawData.slice(0, length);
32-
remainingRawData = remainingRawData.slice(length);
3338
return {
3439
headers: headerMap,
35-
extractedData: data,
40+
remainingRawData,
41+
};
42+
}
43+
44+
export function jsonRPCContent(headers: Map<string, string>, rawData: string): IJSONRPCContent {
45+
const length = parseInt(headers.get('Content-Length') ?? '0', 10);
46+
const data = rawData.slice(0, length);
47+
const remainingRawData = rawData.slice(length);
48+
return {
49+
extractedJSON: data,
3650
remainingRawData,
3751
};
3852
}

src/test/testing/testController/server.unit.test.ts

Lines changed: 19 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
// Licensed under the MIT License.
33

44
import * as assert from 'assert';
5-
import * as http from 'http';
5+
import * as net from 'net';
66
import * as sinon from 'sinon';
77
import * as crypto from 'crypto';
88
import { OutputChannel, Uri } from 'vscode';
99
import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types';
10-
import { createDeferred } from '../../../client/common/utils/async';
1110
import { PythonTestServer } from '../../../client/testing/testController/common/server';
12-
import * as logging from '../../../client/logging';
1311
import { ITestDebugLauncher } from '../../../client/testing/common/types';
14-
import { IJSONRPCMessage, jsonRPCProcessor } from '../../../client/testing/testController/common/utils';
12+
import { createDeferred } from '../../../client/common/utils/async';
1513

1614
suite('Python Test Server', () => {
1715
const fakeUuid = 'fake-uuid';
@@ -22,13 +20,11 @@ suite('Python Test Server', () => {
2220
let sandbox: sinon.SinonSandbox;
2321
let execArgs: string[];
2422
let v4Stub: sinon.SinonStub;
25-
let traceLogStub: sinon.SinonStub;
2623
let debugLauncher: ITestDebugLauncher;
2724

2825
setup(() => {
2926
sandbox = sinon.createSandbox();
3027
v4Stub = sandbox.stub(crypto, 'randomUUID');
31-
traceLogStub = sandbox.stub(logging, 'traceLog');
3228

3329
v4Stub.returns(fakeUuid);
3430
stubExecutionService = ({
@@ -121,157 +117,42 @@ suite('Python Test Server', () => {
121117
});
122118

123119
test('If the server receives malformed data, it should display a log message, and not fire an event', async () => {
120+
let eventData: string | undefined;
121+
const client = new net.Socket();
124122
const deferred = createDeferred();
123+
125124
const options = {
126125
command: { script: 'myscript', args: ['-foo', 'foo'] },
127126
workspaceFolder: Uri.file('/foo/bar'),
128127
cwd: '/foo/bar',
129128
uuid: fakeUuid,
130129
};
131130

132-
let response;
131+
stubExecutionService = ({
132+
exec: async () => {
133+
client.connect(server.getPort());
134+
return Promise.resolve({ stdout: '', stderr: '' });
135+
},
136+
} as unknown) as IPythonExecutionService;
133137

134138
server = new PythonTestServer(stubExecutionFactory, debugLauncher);
135139
await server.serverReady();
136-
137140
server.onDataReceived(({ data }) => {
138-
response = data;
141+
eventData = data;
139142
deferred.resolve();
140143
});
141144

142-
await server.sendCommand(options);
143-
144-
// Send data back.
145-
const port = server.getPort();
146-
const requestOptions = {
147-
hostname: 'localhost',
148-
method: 'POST',
149-
port,
150-
headers: { 'Request-uuid': fakeUuid },
151-
};
152-
153-
const request = http.request(requestOptions, (res) => {
154-
res.setEncoding('utf8');
145+
client.on('connect', () => {
146+
console.log('Socket connected, local port:', client.localPort);
147+
client.write('malformed data');
148+
client.end();
155149
});
156-
const postData = '[test';
157-
request.write(postData);
158-
request.end();
159-
160-
await deferred.promise;
161-
162-
sinon.assert.calledOnce(traceLogStub);
163-
assert.deepStrictEqual(response, '');
164-
});
165-
test('If the server receives data, it should not fire an event if it is an unknown uuid', async () => {
166-
const deferred = createDeferred();
167-
const options = {
168-
command: { script: 'myscript', args: ['-foo', 'foo'] },
169-
workspaceFolder: Uri.file('/foo/bar'),
170-
cwd: '/foo/bar',
171-
};
172-
173-
let response;
174-
175-
server = new PythonTestServer(stubExecutionFactory, debugLauncher);
176-
await server.serverReady();
177-
178-
server.onDataReceived(({ data }) => {
179-
response = data;
180-
deferred.resolve();
150+
client.on('error', (error) => {
151+
console.log('Socket connection error:', error);
181152
});
182153

183154
await server.sendCommand(options);
184-
185-
// Send data back.
186-
const port = server.getPort();
187-
const requestOptions = {
188-
hostname: 'localhost',
189-
method: 'POST',
190-
port,
191-
headers: { 'Request-uuid': fakeUuid },
192-
};
193-
// request.hasHeader()
194-
const request = http.request(requestOptions, (res) => {
195-
res.setEncoding('utf8');
196-
});
197-
const postData = JSON.stringify({ status: 'success', uuid: fakeUuid, payload: 'foo' });
198-
request.write(postData);
199-
request.end();
200-
201155
await deferred.promise;
202-
203-
assert.deepStrictEqual(response, postData);
204-
});
205-
206-
test('If the server receives data, it should not fire an event if there is no uuid', async () => {
207-
const deferred = createDeferred();
208-
const options = {
209-
command: { script: 'myscript', args: ['-foo', 'foo'] },
210-
workspaceFolder: Uri.file('/foo/bar'),
211-
cwd: '/foo/bar',
212-
};
213-
214-
let response;
215-
216-
server = new PythonTestServer(stubExecutionFactory, debugLauncher);
217-
await server.serverReady();
218-
219-
server.onDataReceived(({ data }) => {
220-
response = data;
221-
deferred.resolve();
222-
});
223-
224-
await server.sendCommand(options);
225-
226-
// Send data back.
227-
const port = server.getPort();
228-
const requestOptions = {
229-
hostname: 'localhost',
230-
method: 'POST',
231-
port,
232-
headers: { 'Request-uuid': 'some-other-uuid' },
233-
};
234-
const requestOptions2 = {
235-
hostname: 'localhost',
236-
method: 'POST',
237-
port,
238-
headers: { 'Request-uuid': fakeUuid },
239-
};
240-
const requestOne = http.request(requestOptions, (res) => {
241-
res.setEncoding('utf8');
242-
});
243-
const postDataOne = JSON.stringify({ status: 'success', uuid: 'some-other-uuid', payload: 'foo' });
244-
requestOne.write(postDataOne);
245-
requestOne.end();
246-
247-
const requestTwo = http.request(requestOptions2, (res) => {
248-
res.setEncoding('utf8');
249-
});
250-
const postDataTwo = JSON.stringify({ status: 'success', uuid: fakeUuid, payload: 'foo' });
251-
requestTwo.write(postDataTwo);
252-
requestTwo.end();
253-
254-
await deferred.promise;
255-
256-
assert.deepStrictEqual(response, postDataTwo);
257-
});
258-
test('jsonRPCProcessor', async () => {
259-
const rawDataString = `Content-Length: 160
260-
Content-Type: application/json
261-
Request-uuid: xxxxxx-1012-xxxx-xxxx-xxx
262-
263-
{"cwd": "/path/to/folder", "status": "success", "tests": {"name": "test", "path": "/path/to/test", "type_": "folder", "children": []}], "id_": "/path/to/test"}}`;
264-
const headers = new Map<string, string>([
265-
['Content-Length', '160'],
266-
['Content-Type', 'application/json'],
267-
['Request-uuid', 'xxxxxx-1012-xxxx-xxxx-xxx'],
268-
]);
269-
const expected: IJSONRPCMessage = {
270-
headers,
271-
extractedData: `{"cwd": "/path/to/folder", "status": "success", "tests": {"name": "test", "path": "/path/to/test", "type_": "folder", "children": []}], "id_": "/path/to/test"}}`,
272-
remainingRawData: '',
273-
};
274-
const output: IJSONRPCMessage = jsonRPCProcessor(rawDataString);
275-
assert.deepStrictEqual(output, expected);
156+
assert.deepStrictEqual(eventData, '');
276157
});
277158
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import {
6+
JSONRPC_CONTENT_LENGTH_HEADER,
7+
JSONRPC_CONTENT_TYPE_HEADER,
8+
JSONRPC_UUID_HEADER,
9+
jsonRPCContent,
10+
jsonRPCHeaders,
11+
} from '../../../client/testing/testController/common/utils';
12+
13+
suite('Test Controller Utils: JSON RPC', () => {
14+
test('Empty raw data string', async () => {
15+
const rawDataString = '';
16+
17+
const output = jsonRPCHeaders(rawDataString);
18+
assert.deepStrictEqual(output.headers.size, 0);
19+
assert.deepStrictEqual(output.remainingRawData, '');
20+
});
21+
22+
test('Valid data empty JSON', async () => {
23+
const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`;
24+
25+
const rpcHeaders = jsonRPCHeaders(rawDataString);
26+
assert.deepStrictEqual(rpcHeaders.headers.size, 3);
27+
assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}');
28+
const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData);
29+
assert.deepStrictEqual(rpcContent.extractedJSON, '{}');
30+
});
31+
32+
test('Valid data NO JSON', async () => {
33+
const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`;
34+
35+
const rpcHeaders = jsonRPCHeaders(rawDataString);
36+
assert.deepStrictEqual(rpcHeaders.headers.size, 3);
37+
assert.deepStrictEqual(rpcHeaders.remainingRawData, '');
38+
const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData);
39+
assert.deepStrictEqual(rpcContent.extractedJSON, '');
40+
});
41+
42+
test('Valid data with full JSON', async () => {
43+
// this is just some random JSON
44+
const json =
45+
'{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}';
46+
const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`;
47+
48+
const rpcHeaders = jsonRPCHeaders(rawDataString);
49+
assert.deepStrictEqual(rpcHeaders.headers.size, 3);
50+
assert.deepStrictEqual(rpcHeaders.remainingRawData, json);
51+
const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData);
52+
assert.deepStrictEqual(rpcContent.extractedJSON, json);
53+
});
54+
55+
test('Valid data with multiple JSON', async () => {
56+
const json =
57+
'{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}';
58+
const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`;
59+
const rawDataString2 = rawDataString + rawDataString;
60+
61+
const rpcHeaders = jsonRPCHeaders(rawDataString2);
62+
assert.deepStrictEqual(rpcHeaders.headers.size, 3);
63+
const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData);
64+
assert.deepStrictEqual(rpcContent.extractedJSON, json);
65+
assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString);
66+
});
67+
});

0 commit comments

Comments
 (0)