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 |