Skip to content

[cmake] Don't let tests inherit the terminal of ctest as stdin - #23651

Open
guitargeek wants to merge 1 commit into
root-project:masterfrom
guitargeek:roottest-stdin-removal
Open

guitargeek wants to merge 1 commit into
root-project:masterfrom
guitargeek:roottest-stdin-removal

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

Tests added with ROOTTEST_ADD_TEST are wrapped in coreutils timeout, which moves them into a background process group. When ctest is run from a terminal, the tests inherit it as stdin, and any terminal access from a background process group stops the process with SIGTTOU. The test then hangs until the ctest timeout kills it.

Since Python 3.15, this happens for every doctest: doctest.testmod() creates a pdb.Pdb, whose constructor sets up the new _pyrepl console when stdin is a TTY, which calls tcsetattr() on it. The JupyROOT doctests therefore always timed out when running ctest interactively, while passing in the CI where there is no terminal.

Use /dev/null as stdin for tests that don't specify an input file, like in non-interactive environments.

🤖 Done with the help of AI

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 20h 54m 54s ⏱️
 3 885 tests  3 884 ✅ 0 💤 1 ❌
79 647 runs  79 646 ✅ 0 💤 1 ❌

For more details on these failures, see this check.

Results for commit 494a260.

♻️ This comment has been updated with latest results.

# background process group, so any terminal access (like the tcsetattr()
# in the pdb.Pdb constructor since Python 3.15, which doctest triggers)
# would stop them with SIGTTOU until the test times out.
set(_input INPUT_FILE /dev/null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely effective but is that the canonical ctest way to say there is no access to the terminal? Also don't we have the same/similar problem on Windows?

Related question why is "the pdb.Pdb constructor calling tcsetattr()"? Isn't there a missing argument to the python invocation to set 'this is a batch mode'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the many questions and again looking at one of my Python 3.15 compatibility PRs! Let me try to unpack the concerns and address them one-by-one.

ctest: As far as I can tell there is no dedicated mechanism. ctest passes its own stdin to the tests (checked with a minimal project run from a pty: isatty(0) is true in the test), and there is no test property for it. The driver runs the command with execute_process, which inherits stdin unless INPUT_FILE is given, so INPUT_FILE /dev/null is the standard way to say "no stdin". It's also what tests already get in CI.

Windows: Not affected. The hang comes from POSIX job control: timeout puts the test in a background process group, where reading from or reconfiguring the terminal raises SIGTTIN/SIGTTOU and stops the process. We don't use timeout on Windows (if(TIMEOUT_BINARY AND NOT MSVC)), and the Windows console has no background groups, so I limited the change to UNIX.

pdb: Since 3.15, pdb uses the new _pyrepl line editor when stdin is a TTY (gh-145378), and Pdb.__init__ sets it up eagerly. The resulting UnixConsole calls tcsetattr() on stdin, which triggers SIGTTOU from a background group even though the settings it writes are unchanged. doctest creates a Pdb on every run to support pdb.set_trace() in examples, so every doctest hits this.

There's no batch-mode flag. The closest is the PYTHON_BASIC_REPL env variable, which pdb also honors. But that only fixes this one symptom: the real problem is that tests are given a terminal they don't own, and detaching stdin fixes that for every test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I agree that this is the simplest way forward.

Comment on lines +157 to 158
set(_input INPUT_FILE /dev/null)
endif()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend

Suggested change
set(_input INPUT_FILE /dev/null)
endif()
set(_input INPUT_FILE /dev/null)
else() # or elseif(WIN32)
set(_input INPUT_FILE NUL)
endif()

for symmetry and to avoid surprises.

@pcanal pcanal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Tests added with ROOTTEST_ADD_TEST are wrapped in coreutils timeout,
which moves them into a background process group. When ctest is run
from a terminal, the tests inherit it as stdin, and any terminal access
from a background process group stops the process with SIGTTOU. The test
then hangs until the ctest timeout kills it.

Since Python 3.15, this happens for every doctest: doctest.testmod()
creates a pdb.Pdb, whose constructor sets up the new _pyrepl console
when stdin is a TTY, which calls tcsetattr() on it. The JupyROOT doctests
therefore always timed out when running ctest interactively, while
passing in the CI where there is no terminal.

Use /dev/null as stdin for tests that don't specify an input file, like
in non-interactive environments.

🤖 Done with the help of AI
@guitargeek
guitargeek force-pushed the roottest-stdin-removal branch from 494a260 to 5ef57c3 Compare October 8, 2026 22:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants