Work around test runner changes in click > 8.1.8
Marco Ricci

Marco Ricci commited on 2026-07-05 14:28:33
Zeige 3 geänderte Dateien mit 30 Einfügungen und 5 Löschungen.


More modern versions of click – I believe 8.2.0 and higher, and
definitely in 8.4.0 and higher – implement stdout and stderr capture in
a completely broken way when prompts are involved and the streams are
mixed: mixed streams are not truly interleaved, and prompts cause extra
typed-in whitespace from stdin to appear in stderr as well.  As
a result, the "correct" captured output in a "mixed stdout/stderr" test
runner looks neither like an interactive shell session with TTY I/O nor
like a non-interactive shell session with redirected I/O; it's
a hobgobbled mess of a bit of both.

Work around this for now by supplying the correct broken expected output
for test runners from newer click versions.

But if anything, this reinforces my desire to drop click as a dependency
as soon as possible.  (See the wishlist item "remove-click" in the 0.6
wishlist.  That wish also details *other* API breakage that users have
to wrangle with.)
... ...
@@ -822,6 +822,13 @@ Boo.
822 822
 [1] baked beans
823 823
 Will replace with spam, okay? (Please say "y" or "n".): Boo.
824 824
 """,
825
+            # Some versions of click concatenate stdout and stderr
826
+            # (in the test runner) instead of interleaving them.  This
827
+            # looks okay on-screen, but rather crazy in the test suite.
828
+            """\
829
+[1] baked beans
830
+Boo.
831
+Will replace with spam, okay? (Please say "y" or "n".): """,
825 832
         }, "expected known output"
826 833
 
827 834
     def test_passphrase(
... ...
@@ -38,6 +38,9 @@ DUMMY_KEY1_B64 = data.DUMMY_KEY1_B64
38 38
 DUMMY_KEY2_B64 = data.DUMMY_KEY2_B64
39 39
 
40 40
 
41
+PROG_NAME = cli.PROG_NAME
42
+
43
+
41 44
 class IncompatibleConfiguration(NamedTuple):
42 45
     other_options: list[tuple[str, ...]]
43 46
     needs_service: bool | None
... ...
@@ -189,7 +192,10 @@ for opt, config in SINGLES.items():
189 192
 
190 193
 def is_warning_line(line: str) -> bool:
191 194
     """Return true if the line is a warning line."""
192
-    return " Warning: " in line or " Deprecation warning: " in line
195
+    return line.startswith((
196
+        f"{PROG_NAME}: Warning: ",
197
+        f"{PROG_NAME}: Deprecation warning: ",
198
+    ))
193 199
 
194 200
 
195 201
 def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool:
... ...
@@ -872,8 +878,12 @@ class TestPhraseAndKeyOverriding:
872 878
         )
873 879
         assert not result.stdout.strip(), "expected no program output"
874 880
         assert result.stderr, "expected known error output"
875
-        err_lines = result.stderr.splitlines(False)
876
-        assert err_lines[0].startswith("Passphrase:")
881
+        # First line contains passphrase prompt.  Depending on the click
882
+        # version, either this is a standalone line, or the first warning
883
+        # line is attached to the end.  So it needs special treatment.
884
+        err_lines = result.stderr.splitlines(True)
885
+        prompt_line = err_lines[0]
886
+        assert prompt_line.startswith("Passphrase:")
877 887
         assert machinery.warning_emitted(
878 888
             "Setting a service passphrase is ineffective ",
879 889
             caplog.record_tuples,
... ...
@@ -881,7 +891,11 @@ class TestPhraseAndKeyOverriding:
881 891
             "Setting a global passphrase is ineffective ",
882 892
             caplog.record_tuples,
883 893
         ), "expected known warning message"
884
-        assert all(map(is_warning_line, result.stderr.splitlines(True)))
894
+        remainder = prompt_line.removeprefix("Passphrase:").lstrip()
895
+        other_lines = (
896
+            [remainder, *err_lines[1:]] if remainder else err_lines[1:]
897
+        )
898
+        assert all(map(is_warning_line, other_lines))
885 899
         assert all(
886 900
             map(is_harmless_config_import_warning, caplog.record_tuples)
887 901
         ), "unexpected error output"
... ...
@@ -701,7 +701,11 @@ class TestStoringConfigurationSuccesses:
701 701
             result_config={"global": {"phrase": "abc"}, "services": {}},
702 702
             input="abc\n",
703 703
         )
704
-        assert result.stderr == "Passphrase:", "program unexpectedly failed?!"
704
+        # Some versions of click leave the space and/or the line
705
+        # terminator in stderr when running in the test runner.
706
+        assert result.stderr.strip() == "Passphrase:", (
707
+            "program unexpectedly failed?!"
708
+        )
705 709
 
706 710
 
707 711
 class TestStoringConfigurationFailures:
708 712