Skip to content

test: stop a leftover soffice from hanging the test run (#1158) - #1160

Merged
JSv4 merged 3 commits into
mainfrom
fix/issue-1158
Oct 16, 2026
Merged

JSv4 merged 3 commits into
mainfrom
fix/issue-1158

Conversation

@JSv4

@JSv4 JSv4 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Closes #1158

Why

A full dotnet test run sat idle for 80 minutes inside the header/footer comparison smoke test. The test starts a Python script that drives LibreOffice, reads the script's output to the end, and only then waits for the script with a two-minute timeout. Reading to the end only finishes when every process holding the output pipe has closed it. The script started soffice with that same output, and when its shutdown call timed out it killed only the soffice shell wrapper; the soffice.bin it had launched kept running and kept the pipe open. So the read never finished and the timeout never ran.

How it works

Both halves the issue suggested are done, so either one alone would have prevented the hang.

Test side. A new helper, Docxodus.Tests/TestProcess.cs, runs a child process the safe way: it reads stdout and stderr on two dedicated background threads (not thread-pool tasks, which a busy test host can starve), waits for the process with a deadline, kills the whole process tree when the deadline passes, and then gives the readers a short grace period (5 s). If a leftover process still holds the pipe after that, the helper returns what it has read and reports the output as incomplete instead of waiting. The three LibreOffice oracle runners (header/footer smoke, comment real-document, block-format real-document) now use it. The helper also sets PYTHONDONTWRITEBYTECODE so the scripts don't leave __pycache__ in the source tree.

Script side. A new module, tools/diffharness/lo/lo_soffice.py, starts every soffice with /dev/null for its standard streams and in its own process group, and stops it by signalling the whole group (SIGTERM, wait for the group to empty, then SIGKILL). The --unaccept shutdown call runs the same way and is killed as a group if it takes longer than 10 s. The six UNO oracle scripts (header/footer, comment, bookmark, footnote, compare, batch) use it; their LibreOffice logic is unchanged.

Validation

  • New .NET tests (TestProcessTests): a shell that exits while a background sleep 60 holds its stdout returns within the bound; a process past its deadline is killed with its output kept; stdout/stderr are both collected. With the helper mutated back to read-to-end-before-wait, two of them fail after hanging the full 60 s.
  • New Python tests (test_lo_soffice.py): a fake soffice whose child outlives it no longer holds the caller's pipe; stopping the listener ends what it started; a detached command is killed as a group at its timeout. Removing the /dev/null redirection or the separate session makes them fail.
  • The real oracles run green against LibreOffice 25.8.7.3 locally, with no soffice.bin left afterwards. All six rewired scripts were smoke-run against a fixture.
  • Python suite under tools/diffharness/lo: 17 tests pass.

Adversarial review

One round of hostile review ran on the diff before opening. It confirmed against the real LibreOffice launcher (soffice → oosplash → soffice.bin, all in one group) that the group kill reaches soffice.bin, and that a no-listener --unaccept (the orphan from the issue) is now killed after 10 s. It found four low-severity problems, all fixed in the second commit: the CHANGELOG overclaimed "every script" (lo_render.py manages its own processes); the oracle runs left __pycache__ in the tree; a failing unaccept call could skip stopping the listener; and the SIGKILL grace waited on the wrapper rather than the whole group. The first CI run then exposed a fifth problem: the readers were thread-pool tasks, and on CI's busy test host they had not run within the one-second drain grace, so the descendant test saw empty output. The third commit moves each reader to its own thread. Two other spawn sites with lower exposure (SourceFileCopyrightTests running git, and the history-recovery probe) were left as they are.

JSv4 added 3 commits October 16, 2026 00:31
The LibreOffice oracles read their script's output to the end before
waiting with a timeout, and the scripts started soffice with that same
output. A soffice.bin orphaned by a timed-out shutdown call kept the
pipe open, so the read never returned and the timeout never ran.

Tests now run the scripts through TestProcess, which reads on background
tasks, kills the process tree at the deadline and stops waiting on a pipe
a leftover process still holds. Every LibreOffice script starts and stops
soffice through lo_soffice.py, with no inherited output, its own process
group, and a whole-group stop.
Guard the scripts' shutdown so a failed unaccept still stops the
listener, wait for the whole process group before escalating to SIGKILL,
keep Python bytecode caches out of the source tree, and name the six
scripts the change covers.
CI's busy test host starved the thread pool for longer than the drain
grace, so a pool-task reader had read nothing when the helper gave up.
Each output pump is now its own background thread.
@JSv4
JSv4 merged commit eb62ec8 into main Oct 16, 2026
16 checks passed
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.

LibreOffice header/footer oracle can hang the whole test suite: stdout is read to end before the exit timeout

1 participant