Skip to content

Commit 2b8c805

Browse files
committed
Test
1 parent cc6d398 commit 2b8c805

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

test/test_python_lsp.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import socket
2+
import threading
3+
import base64
4+
import hashlib
5+
import json
6+
import struct
7+
import pytest
8+
import time
9+
from pylsp.python_lsp import start_ws_lang_server, PythonLSPServer
10+
11+
WS_PORT = 3002
12+
NUM_REQUESTS = 20 # enough to provoke interleaving
13+
14+
# --- Helpers for raw WebSocket framing ---
15+
16+
def make_ws_key():
17+
raw = hashlib.sha1(str(time.time()).encode()).digest()[:16]
18+
return base64.b64encode(raw).decode()
19+
20+
21+
def build_handshake(host='localhost', port=WS_PORT):
22+
key = make_ws_key()
23+
return (
24+
f"GET / HTTP/1.1\r\n"
25+
f"Host: {host}:{port}\r\n"
26+
f"Upgrade: websocket\r\n"
27+
f"Connection: Upgrade\r\n"
28+
f"Sec-WebSocket-Key: {key}\r\n"
29+
f"Sec-WebSocket-Version: 13\r\n"
30+
f"\r\n"
31+
).encode()
32+
33+
34+
def build_frame(text: str) -> bytes:
35+
data = text.encode('utf8')
36+
length = len(data)
37+
header = b"\x81"
38+
if length < 126:
39+
header += struct.pack('B', length)
40+
elif length < (1 << 16):
41+
header += b"\x7e" + struct.pack('!H', length)
42+
else:
43+
header += b"\x7f" + struct.pack('!Q', length)
44+
return header + data
45+
46+
47+
# Helper to run the LSP server in a background thread
48+
def run_server():
49+
# Use the actual PythonLSPServer class for handler_class
50+
start_ws_lang_server(host='127.0.0.1', port=WS_PORT, check_parent_process=False, handler_class=PythonLSPServer)
51+
52+
@pytest.fixture(scope="module", autouse=True)
53+
def server():
54+
# Start the server thread
55+
thread = threading.Thread(target=run_server, daemon=True)
56+
thread.start()
57+
# Give server time to start
58+
time.sleep(5)
59+
yield
60+
# Daemon thread will exit with test process
61+
62+
63+
def test_raw_frame_json_integrity():
64+
# 1) Open raw TCP socket and perform WebSocket handshake
65+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
66+
sock.connect(('127.0.0.1', WS_PORT))
67+
sock.sendall(build_handshake())
68+
resp = b''
69+
while b'\r\n\r\n' not in resp:
70+
resp += sock.recv(1024)
71+
72+
# 2) Send frames concurrently
73+
def send_req(i):
74+
msg = {"jsonrpc":"2.0","id":i,"method":"initialize","params":{}}
75+
sock.sendall(build_frame(json.dumps(msg, ensure_ascii=False)))
76+
77+
threads = []
78+
for i in range(1, NUM_REQUESTS + 1):
79+
t = threading.Thread(target=send_req, args=(i,))
80+
t.start()
81+
threads.append(t)
82+
for t in threads:
83+
t.join()
84+
85+
# 3) Read and validate responses
86+
received_ids = set()
87+
buffer = b''
88+
while len(received_ids) < NUM_REQUESTS:
89+
chunk = sock.recv(4096)
90+
if not chunk:
91+
break
92+
buffer += chunk
93+
# parse frames
94+
while True:
95+
if len(buffer) < 2:
96+
break
97+
b1, b2 = buffer[0], buffer[1]
98+
length = b2 & 0x7f
99+
offset = 2
100+
if length == 126:
101+
if len(buffer) < offset + 2: break
102+
length = struct.unpack('!H', buffer[offset:offset+2])[0]; offset += 2
103+
elif length == 127:
104+
if len(buffer) < offset + 8: break
105+
length = struct.unpack('!Q', buffer[offset:offset+8])[0]; offset += 8
106+
if b2 & 0x80:
107+
offset += 4
108+
if len(buffer) < offset + length:
109+
break
110+
payload = buffer[offset:offset+length]
111+
buffer = buffer[offset+length:]
112+
try:
113+
obj = json.loads(payload.decode('utf8'))
114+
except json.JSONDecodeError:
115+
pytest.fail(f"Invalid JSON frame: {payload}")
116+
received_ids.add(obj.get('id'))
117+
assert received_ids == set(range(1, NUM_REQUESTS + 1)), f"Expected IDs 1..{NUM_REQUESTS}, got {received_ids}"
118+
sock.close()

0 commit comments

Comments
 (0)