23803f14ec
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 4m0s
CI / docker (pull_request) Successful in 38s
CI / integration_tests (pull_request) Successful in 4m48s
CI / coverage (pull_request) Successful in 4m31s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 15s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m27s
CI / integration_tests (push) Successful in 3m3s
CI / docker (push) Successful in 38s
CI / coverage (push) Successful in 4m25s
CI / benchmark-publish (push) Successful in 16m28s
CI / benchmark-regression (pull_request) Successful in 28m52s
Added minimal LSP server entrypoint supporting initialize/shutdown/exit handshake over JSON-RPC stdin/stdout transport with Content-Length framing. Unsupported methods return MethodNotFound error with descriptive message. Wired LSP requests through ACP facade in local mode. Added agents lsp serve CLI command with --log-level flag, PID output, and startup banner. Created reference documentation for the stub server. Includes Behave BDD tests for protocol handshake, Robot smoke test, and ASV startup latency benchmark. ISSUES CLOSED: #203
159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
"""Mock stdin/stdout transport for LSP server testing.
|
|
|
|
Provides :class:`MockLspTransport` which simulates the
|
|
Content-Length framed JSON-RPC transport used by the LSP server
|
|
without requiring a real subprocess.
|
|
|
|
The :func:`parse_lsp_responses` utility is shared with the Robot
|
|
Framework helper to avoid duplicating response-parsing logic.
|
|
|
|
All mocks reside in ``features/mocks/`` per project conventions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
def parse_lsp_responses(raw: bytes) -> list[dict[str, Any]]:
|
|
"""Parse Content-Length framed JSON-RPC responses from raw bytes.
|
|
|
|
Shared utility used by :class:`MockLspTransport` and the Robot
|
|
Framework helper to avoid duplicating the parsing logic.
|
|
|
|
Args:
|
|
raw: The raw bytes containing one or more Content-Length
|
|
framed JSON-RPC response messages.
|
|
|
|
Returns:
|
|
List of parsed JSON-RPC response dicts.
|
|
"""
|
|
responses: list[dict[str, Any]] = []
|
|
offset = 0
|
|
while offset < len(raw):
|
|
header_end = raw.find(b"\r\n\r\n", offset)
|
|
if header_end == -1:
|
|
break
|
|
header_section = raw[offset:header_end].decode("utf-8", errors="replace")
|
|
content_length: int | None = None
|
|
for line in header_section.split("\r\n"):
|
|
if line.lower().startswith("content-length:"):
|
|
content_length = int(line.split(":", 1)[1].strip())
|
|
if content_length is None:
|
|
break
|
|
body_start = header_end + 4
|
|
body_end = body_start + content_length
|
|
body = raw[body_start:body_end]
|
|
responses.append(json.loads(body))
|
|
offset = body_end
|
|
return responses
|
|
|
|
|
|
class MockLspTransport:
|
|
"""Simulated LSP stdin/stdout binary streams.
|
|
|
|
Encodes JSON-RPC messages with Content-Length framing into a
|
|
binary input buffer and captures binary output for assertion.
|
|
|
|
Usage::
|
|
|
|
transport = MockLspTransport()
|
|
transport.send_request("initialize", {"capabilities": {}}, id=1)
|
|
server = LspServer(
|
|
input_stream=transport.input_stream,
|
|
output_stream=transport.output_stream,
|
|
)
|
|
server.run()
|
|
responses = transport.read_responses()
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._input_buf = io.BytesIO()
|
|
self._output_buf = io.BytesIO()
|
|
|
|
def send_request(
|
|
self,
|
|
method: str,
|
|
params: dict[str, Any] | None = None,
|
|
request_id: int | str | None = 1,
|
|
) -> None:
|
|
"""Append a JSON-RPC request to the input buffer.
|
|
|
|
Args:
|
|
method: The JSON-RPC method name.
|
|
params: Optional parameters dict.
|
|
request_id: The request id (``None`` for notifications).
|
|
"""
|
|
msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
|
|
if request_id is not None:
|
|
msg["id"] = request_id
|
|
if params is not None:
|
|
msg["params"] = params
|
|
self._append_message(msg)
|
|
|
|
def send_notification(
|
|
self,
|
|
method: str,
|
|
params: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""Append a JSON-RPC notification (no id) to the input buffer.
|
|
|
|
Args:
|
|
method: The JSON-RPC method name.
|
|
params: Optional parameters dict.
|
|
"""
|
|
self.send_request(method, params, request_id=None)
|
|
|
|
def send_raw(self, raw_bytes: bytes) -> None:
|
|
"""Append raw bytes to the input buffer.
|
|
|
|
Useful for testing malformed messages.
|
|
|
|
Args:
|
|
raw_bytes: The raw bytes to append.
|
|
"""
|
|
pos = self._input_buf.tell()
|
|
self._input_buf.seek(0, 2)
|
|
self._input_buf.write(raw_bytes)
|
|
self._input_buf.seek(pos)
|
|
|
|
def _append_message(self, msg: dict[str, Any]) -> None:
|
|
"""Encode and append a Content-Length framed message."""
|
|
payload = json.dumps(msg).encode("utf-8")
|
|
header = f"Content-Length: {len(payload)}\r\n\r\n".encode()
|
|
pos = self._input_buf.tell()
|
|
self._input_buf.seek(0, 2)
|
|
self._input_buf.write(header + payload)
|
|
self._input_buf.seek(pos)
|
|
|
|
@property
|
|
def input_stream(self) -> io.BytesIO:
|
|
"""Return the input stream positioned at the start."""
|
|
self._input_buf.seek(0)
|
|
return self._input_buf
|
|
|
|
@property
|
|
def output_stream(self) -> io.BytesIO:
|
|
"""Return the output stream for writing."""
|
|
return self._output_buf
|
|
|
|
def read_responses(self) -> list[dict[str, Any]]:
|
|
"""Parse all JSON-RPC responses from the output buffer.
|
|
|
|
Returns:
|
|
List of parsed JSON-RPC response dicts.
|
|
"""
|
|
self._output_buf.seek(0)
|
|
raw = self._output_buf.read()
|
|
if not raw:
|
|
return []
|
|
return parse_lsp_responses(raw)
|
|
|
|
|
|
__all__ = [
|
|
"MockLspTransport",
|
|
"parse_lsp_responses",
|
|
]
|