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