/ tests / test_install_sh_setup_wizard_tty_probe.py
test_install_sh_setup_wizard_tty_probe.py
 1  """Regression for #16746: install.sh /dev/tty gates must actually open /dev/tty.
 2  
 3  In a Docker build, ``/dev/tty`` exists as a device node (so a bare ``-e``
 4  existence test returns true) but opening it fails with ``ENXIO: No such
 5  device or address``. Under the old gates the script proceeded past the "no
 6  terminal available" skip and then crashed on the ``< /dev/tty`` redirect a
 7  few lines later, aborting the entire image build. The fix replaces every
 8  existence-based check that guards a subsequent ``< /dev/tty`` redirect with
 9  an open-based probe so the skip kicks in correctly.
10  
11  This module covers all three affected functions: ``run_setup_wizard()``
12  (the reproducer in #16746), ``install_system_packages()`` (the apt sudo
13  prompt fallback), and ``maybe_start_gateway()`` (the gateway-install gate).
14  """
15  
16  from __future__ import annotations
17  
18  import re
19  from pathlib import Path
20  
21  import pytest
22  
23  REPO_ROOT = Path(__file__).resolve().parent.parent
24  INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
25  
26  # Every function in scripts/install.sh that previously gated on a bare
27  # ``[ -e /dev/tty ]`` check before redirecting stdin from ``/dev/tty``.
28  GATED_FUNCTIONS = ("run_setup_wizard", "install_system_packages", "maybe_start_gateway")
29  
30  
31  def _extract_function_body(name: str) -> str:
32      """Return the body of ``<name>()`` as a single string.
33  
34      Anchored to ``<name>()`` and a top-of-line ``}`` so the helper keeps
35      working if neighbouring functions are renamed.
36      """
37      text = INSTALL_SH.read_text()
38      match = re.search(
39          rf"^{re.escape(name)}\(\)\s*\{{\s*\n(?P<body>.*?)^\}}",
40          text,
41          re.MULTILINE | re.DOTALL,
42      )
43      assert match is not None, f"{name}() not found in scripts/install.sh"
44      return match["body"]
45  
46  
47  @pytest.mark.parametrize("fn_name", GATED_FUNCTIONS)
48  def test_tty_gate_does_not_use_existence_only_check(fn_name: str) -> None:
49      """The bare ``-e`` test is the bug — no spelling of it should remain."""
50      body = _extract_function_body(fn_name)
51      # Cover ``[ -e /dev/tty ]``, ``[ -e "/dev/tty" ]``, ``test -e /dev/tty``
52      # and friends, with arbitrary surrounding whitespace.
53      pattern = re.compile(
54          r"""(
55              \[\s*-e\s+["']?/dev/tty["']?\s*\]
56              |
57              \btest\s+-e\s+["']?/dev/tty["']?
58          )""",
59          re.VERBOSE,
60      )
61      match = pattern.search(body)
62      assert match is None, (
63          f"{fn_name} contains an existence-only check on /dev/tty "
64          f"({match.group(0)!r}). Bare `-e` tests pass in Docker builds "
65          "where the device node is in the mount namespace but cannot be "
66          "opened (ENXIO). Use an open-based probe (e.g. "
67          "`(: </dev/tty) 2>/dev/null` or `exec 3</dev/tty`) so the skip "
68          "kicks in before the function tries to read from /dev/tty. "
69          "See #16746."
70      )
71  
72  
73  @pytest.mark.parametrize("fn_name", GATED_FUNCTIONS)
74  def test_tty_gate_uses_open_based_probe(fn_name: str) -> None:
75      """The gate must actually attempt to open ``/dev/tty``.
76  
77      Any ``if``/``if !``/``elif`` whose condition opens ``/dev/tty`` for
78      input counts: ``(: </dev/tty)``, ``exec 3</dev/tty``,
79      ``{ exec 3</dev/tty; }``, etc. Asserting the higher-level invariant
80      rather than a specific spelling so equivalent refactors stay green.
81      """
82      body = _extract_function_body(fn_name)
83      gate = re.compile(
84          r"^\s*(?:if|elif)\s+!?\s*[^\n]*<\s*/dev/tty[^\n]*;\s*then",
85          re.MULTILINE,
86      )
87      assert gate.search(body), (
88          f"{fn_name} must gate on an open-based probe of /dev/tty "
89          "(an `if`/`if !`/`elif` whose test redirects stdin from /dev/tty), "
90          "not a mere existence check. See #16746."
91      )