Skip to content

Bound the control-mode wait off the descriptor - #733

Open
tony wants to merge 2 commits into
masterfrom
fix/731-control-mode-select
Open

Bound the control-mode wait off the descriptor#733
tony wants to merge 2 commits into
masterfrom
fix/731-control-mode-select

Conversation

@tony

@tony tony commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes #731.

Summary

  • Fix test_control_mode_stdout_preserves_non_ascii_output timing out while the line it wants sits in the buffer. It bounded its wait with select on the descriptor and read with readline, which serves from the TextIOWrapper above it.
  • Replace select with a reader thread and a queue polled against a monotonic deadline, so the wait is bounded without asking the descriptor a question it cannot answer.
  • Leave ControlMode untouched. text=True, encoding="utf-8" is exactly the behaviour this test exists to guard.

Why the descriptor cannot answer

tmux writes a whole %begin/%end block in one burst, so the reply arrives together. The first readline drains up to 8192 bytes off the descriptor into userspace and returns one line; the rest sits in the decoder, and the descriptor is now empty. select reports not-ready while the answer is microseconds away:

select NOT ready at iter=1 (last read = '%begin 1785458429 355 1')
  readline #0 returned in 7us  -> '␞'
  readline #1 returned in 3us  -> '%end 1785458429 355 1'
  readline #2 BLOCKED (fd empty)

What made the old test pass was unrelated %output from the pane's shell painting its prompt, arriving ~460 ms later and re-arming select. The test passed for a reason unconnected to the data it asserts on, which is why it failed on loaded CI runners: once the read loop starts after that burst, failure is certain rather than unlikely.

Before / After

Injecting a 300 ms stall before the read loop, everything else verbatim:

before:  delay=0.0  0/4 failed   delay=0.15  1/4 failed   delay=0.3  3/4 failed
after:   delay=0.3  0/4 failed

The coverage is preserved

The risk in rewriting this test is silently retiring what it guards. Verified by removing encoding="utf-8" from ControlMode and re-running:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 107: ordinal not in range(128)
1 failed

Still caught. Restored, it passes.

Design notes

A thread rather than select on stdout.buffer.raw. Both work. Selecting on the raw descriptor means the test owns partial-line assembly and decoding — real logic, in a test whose subject is decoding. The thread keeps readline doing the line splitting and adds only a bound.

Monotonic deadline, not a per-iteration timeout. A fixed timeout per line lets a slow-but-progressing stream run for limit × timeout. The deadline bounds the whole read.

Fails, never hangs. Stream exhaustion is reported distinctly from expiry, so a closed pipe does not present as a timeout.

Test plan

  • tests/test_control_mode.py green, 3 consecutive runs at --reruns 0
  • Survives a 300 ms injected stall, 4/4, where the previous version failed 3/4
  • Still fails with encoding="utf-8" removed from ControlMode
  • uv run ruff check . / uv run ruff format . / uv run mypy clean
  • uv run py.test --reruns 0 clean
  • just build-docs clean

Note for the merger

CHANGES will conflict with the sibling PRs for #723, #724, #725 and #726, which all add to the same block — keep both sides.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.45%. Comparing base (be7c8c1) to head (8c54de1).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #733   +/-   ##
=======================================
  Coverage   52.45%   52.45%           
=======================================
  Files          26       26           
  Lines        3729     3729           
  Branches      747      747           
=======================================
  Hits         1956     1956           
  Misses       1469     1469           
  Partials      304      304           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony

tony commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
tony force-pushed the fix/731-control-mode-select branch from 9f3e1c4 to 23592e0 Compare August 1, 2026 01:04
@tony

tony commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

why: The test asked select whether the descriptor had a line and then
read with readline, which serves from the buffer above it. ControlMode
runs its subprocess with text=True, and tmux writes a whole
%begin/%end block in one burst, so the first readline routinely drains
every remaining line off the descriptor -- select then reports nothing
ready while the answer is already in hand. Passing at all depended on
unrelated %output from the pane's shell re-arming select. Closes #731.

what:
- Read lines on a thread and poll a queue with a monotonic deadline, so
  the wait is bounded without consulting the descriptor
- Explain in the helper why the descriptor cannot answer the question
- Leave ControlMode alone: text=True and encoding="utf-8" are the
  behaviour this test guards
@tony
tony force-pushed the fix/731-control-mode-select branch from 23592e0 to 25226cc Compare August 1, 2026 10:40
why: A test that fails on loaded runners and passes on re-run costs
every contributor a re-run, so the entry belongs in the release notes
even though no library code changed.

what:
- Record the bounded control-mode wait under `Development`, where a
  test-suite-only change belongs.
@tony
tony force-pushed the fix/731-control-mode-select branch from 25226cc to 8c54de1 Compare August 1, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test_control_mode_stdout_preserves_non_ascii_output polls select on a buffered stream

1 participant