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