22831e4f50
CI / lint (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 1m30s
CI / security (pull_request) Successful in 1m32s
CI / helm (pull_request) Successful in 29s
CI / build (pull_request) Successful in 33s
CI / push-validation (pull_request) Successful in 30s
CI / integration_tests (pull_request) Successful in 2m57s
CI / unit_tests (pull_request) Successful in 4m38s
CI / docker (pull_request) Successful in 1m25s
CI / coverage (pull_request) Successful in 13m7s
CI / status-check (pull_request) Successful in 6s
The _info_raises side_effect previously raised RuntimeError on every logger.info call, which caused the pre-Popen "lsp.transport.starting" call (transport.py:109) to raise before subprocess.Popen was ever reached. This meant the cleanup guard at lines 160-186 was never exercised, making the scenario a permanent no-op TDD stub rather than a genuine regression guard. Fix: make _info_raises conditional on the first positional argument being "lsp.transport.started" (the post-Popen message). The pre-Popen "lsp.transport.starting" call now passes through normally, Popen succeeds (mocked), and the RuntimeError is raised inside the guarded try/except block, triggering terminate() + wait() cleanup. Remove @tdd_expected_fail from the scenario since the cleanup code at transport.py:160-186 is in place and the scenario now passes. Closes #7044
175 lines
8.7 KiB
Gherkin
175 lines
8.7 KiB
Gherkin
Feature: LSP StdioTransport coverage
|
|
As a developer maintaining the LSP transport layer
|
|
I need thorough tests for StdioTransport
|
|
So that all error paths and normal flows are exercised
|
|
|
|
# ── start() error paths ─────────────────────────────────────────
|
|
|
|
@tdd_issue @tdd_issue_7044
|
|
Scenario: ltcov start cleans up subprocess when post-Popen init fails
|
|
Given ltcov I create a StdioTransport for command "cat"
|
|
And ltcov Popen is mocked to succeed with a running process
|
|
And ltcov logger.info after spawn raises RuntimeError
|
|
When ltcov I try to start the transport
|
|
Then ltcov an error occurred during start()
|
|
And ltcov the internal process should be None
|
|
And ltcov the mock process should have been terminated
|
|
And ltcov the mock process should have been waited on
|
|
|
|
Scenario: ltcov start raises RuntimeError when already alive
|
|
Given ltcov I create a StdioTransport for command "cat"
|
|
And ltcov the transport has a running mock process
|
|
When ltcov I try to start the transport
|
|
Then ltcov the error should be a RuntimeError with message "already started"
|
|
|
|
@tdd_issue @tdd_issue_7044
|
|
Scenario: ltcov start is alive after successful spawn
|
|
Given ltcov I create a StdioTransport for command "cat"
|
|
When ltcov I try to start the transport
|
|
Then ltcov the transport should be alive
|
|
And ltcov the internal process should not be None
|
|
|
|
Scenario: ltcov is_alive returns False when no process
|
|
Given ltcov I create a StdioTransport for command "cat"
|
|
When ltcov I check if the transport is alive
|
|
Then ltcov the is_alive result should be false
|
|
|
|
Scenario: ltcov start raises LspError on FileNotFoundError
|
|
Given ltcov I create a StdioTransport for command "nonexistent_binary_xyz"
|
|
And ltcov Popen is mocked to raise FileNotFoundError
|
|
When ltcov I try to start the transport
|
|
Then ltcov the error should be an LspError with message "not found"
|
|
|
|
Scenario: ltcov start raises LspError on OSError
|
|
Given ltcov I create a StdioTransport for command "bad_command"
|
|
And ltcov Popen is mocked to raise OSError with "Permission denied"
|
|
When ltcov I try to start the transport
|
|
Then ltcov the error should be an LspError with message "Failed to start"
|
|
|
|
# ── stop() paths ────────────────────────────────────────────────
|
|
|
|
Scenario: ltcov stop returns None when not started
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
When ltcov I stop the transport
|
|
Then ltcov the stop result should be None
|
|
|
|
Scenario: ltcov stop returns exit code when process already exited
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process that already exited with code 0
|
|
When ltcov I stop the transport
|
|
Then ltcov the stop result should be 0
|
|
And ltcov the internal process should be None
|
|
|
|
Scenario: ltcov stop returns exit code when process already exited with error
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process that already exited with code 1
|
|
When ltcov I stop the transport
|
|
Then ltcov the stop result should be 1
|
|
|
|
Scenario: ltcov stop force kills on timeout
|
|
Given ltcov I create a StdioTransport for command "sleep"
|
|
And ltcov the transport has a mock process that ignores terminate
|
|
When ltcov I stop the transport with timeout 0.1
|
|
Then ltcov the mock process should have been killed
|
|
And ltcov the stop result should be an integer
|
|
|
|
# ── send_message() paths ────────────────────────────────────────
|
|
|
|
Scenario: ltcov send_message raises RuntimeError when not started
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
When ltcov I try to send a message
|
|
Then ltcov the error should be a RuntimeError with message "not started"
|
|
|
|
Scenario: ltcov send_message raises RuntimeError when stdin is None
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with no stdin
|
|
When ltcov I try to send a message
|
|
Then ltcov the error should be a RuntimeError with message "not started"
|
|
|
|
Scenario: ltcov send_message writes Content-Length framed JSON-RPC
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with writable stdin
|
|
When ltcov I send the message {"jsonrpc":"2.0","method":"initialize","id":1}
|
|
Then ltcov the written bytes should contain "Content-Length:"
|
|
And ltcov the written bytes should contain "initialize"
|
|
|
|
Scenario: ltcov send_message raises BrokenPipeError on dead process
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with broken stdin
|
|
When ltcov I try to send a message
|
|
Then ltcov the error should be a BrokenPipeError
|
|
|
|
# ── read_message() paths ────────────────────────────────────────
|
|
|
|
Scenario: ltcov read_message raises RuntimeError when not started
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
When ltcov I try to read a message
|
|
Then ltcov the error should be a RuntimeError with message "not started"
|
|
|
|
Scenario: ltcov read_message raises RuntimeError when stdout is None
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with no stdout
|
|
When ltcov I try to read a message
|
|
Then ltcov the error should be a RuntimeError with message "not started"
|
|
|
|
Scenario: ltcov read_message returns None on select timeout
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with empty stdout
|
|
And ltcov select is mocked to return not ready
|
|
When ltcov I read a message with timeout 1.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message returns None on EOF
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with stdout that returns EOF
|
|
When ltcov I read a message with timeout 1.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message uses default timeout when None
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with stdout that returns EOF
|
|
When ltcov I read a message with default timeout
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message parses valid Content-Length message
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with a valid JSON-RPC response
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should have key "jsonrpc" with value "2.0"
|
|
|
|
Scenario: ltcov read_message returns None on invalid content-length value
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with invalid content-length header
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message returns None when no content-length header
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with no content-length header
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message raises ValueError on oversized message
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with oversized content-length
|
|
When ltcov I try to read a message
|
|
Then ltcov the error should be a ValueError with message "too large"
|
|
|
|
Scenario: ltcov read_message returns None on body select timeout
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with headers but body times out
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message returns None on truncated body
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with truncated body
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should be None
|
|
|
|
Scenario: ltcov read_message returns None on invalid JSON body
|
|
Given ltcov I create a StdioTransport for command "echo"
|
|
And ltcov the transport has a mock process with invalid JSON body
|
|
When ltcov I read a message with timeout 5.0
|
|
Then ltcov the read result should be None
|