c8232c17f4
When subprocess.Popen() fails during LSP server initialization (e.g. FileNotFoundError for missing command or general OSError), partially-allocated pipe resources and internal file descriptors could be left in an inconsistent state. This was caused by _process potentially containing a stale reference if an exception occurred between pipe allocation and the Popen object being fully returned. Fix: Add explicit self._process = None resets in three places: 1) Before subprocess.Popen() — ensures clean initial state even across retries 2) In FileNotFoundError handler — guards against intermediate error states 3) In OSError handler — general safety net for all subprocess failures This prevents: - Orphaned child processes never terminated (zombie processes) - File descriptor leaks from partially-allocated pipes - Transports stuck in ambiguous 'started but not live' state Tests added: Two new Behave scenarios verify _process is None after both FileNotFoundError and OSError during start(). ISSUES CLOSED: #10597
191 lines
9.5 KiB
Gherkin
191 lines
9.5 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
|
|
|
|
# ── subprocess cleanup on failed initialization (issue #10597) ────────────
|
|
|
|
Scenario: ltcov _process is reset to None after FileNotFoundError in start
|
|
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"
|
|
And ltcov _process must be None after start failure
|
|
|
|
Scenario: ltcov _process is reset to None after OSError in start
|
|
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"
|
|
And ltcov _process must be None after start failure
|