Skip to content

Commit 62c0d29

Browse files
Mitigate testing discrepancies from click 8.2.0 and below
A discrepancy exists between the documentation of `click.prompt` and the actual behavior of `click.prompt` when mocked with `click.testing.CliRunner` on click 8.2.0 and below ([`pallets/click#2934`][BUG2934]): when prompting for input and if at end-of-file, the `CliRunner` may return an empty string instead of raising `click.Abort`. This usually translates to extra line breaks in the "mixed" and "echoed" runner output at the last prompt(s). We mitigate this discrepancy from both sides. On the code side, we wrap each call to `click.prompt` to treat aborts and empty responses the same, which is appropriate behavior for the types of prompts we issue. On the test side, we amend our existing tests to use empty-line input instead of no input, and explicitly test the "no input" scenario separately, accepting both the 8.2.0-or-lower or the 8.2.1-or-higher output. Because the behavior depends on the `click` version, which is beyond our control, we also adjust coverage measurement. [BUG2934]: pallets/click#2934
1 parent 7ecfed1 commit 62c0d29

File tree

2 files changed

+113
-22
lines changed

2 files changed

+113
-22
lines changed

src/derivepassphrase/_internals/cli_helpers.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -657,14 +657,24 @@ def prompt_for_selection(
657657
click.echo(x, err=True, color=color)
658658
if n > 1:
659659
choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
660-
choice = click.prompt(
661-
f'Your selection? (1-{n}, leave empty to abort)',
662-
err=True,
663-
type=choices,
664-
show_choices=False,
665-
show_default=False,
666-
default='',
667-
)
660+
try:
661+
choice = click.prompt(
662+
f'Your selection? (1-{n}, leave empty to abort)',
663+
err=True,
664+
type=choices,
665+
show_choices=False,
666+
show_default=False,
667+
default='',
668+
)
669+
except click.Abort: # pragma: no cover
670+
# This branch will not be triggered during testing on
671+
# `click` versions < 8.2.1, due to (non-monkeypatch-able)
672+
# deficiencies in `click.testing.CliRunner`. Therefore, as
673+
# an external source of nondeterminism, exclude it from
674+
# coverage.
675+
#
676+
# https://github.com/pallets/click/issues/2934
677+
choice = ''
668678
if not choice:
669679
raise IndexError(EMPTY_SELECTION)
670680
return int(choice) - 1
@@ -763,16 +773,25 @@ def prompt_for_passphrase() -> str:
763773
The user input.
764774
765775
"""
766-
return cast(
767-
'str',
768-
click.prompt(
769-
'Passphrase',
770-
default='',
771-
hide_input=True,
772-
show_default=False,
773-
err=True,
774-
),
775-
)
776+
try:
777+
return cast(
778+
'str',
779+
click.prompt(
780+
'Passphrase',
781+
default='',
782+
hide_input=True,
783+
show_default=False,
784+
err=True,
785+
),
786+
)
787+
except click.Abort: # pragma: no cover
788+
# This branch will not be triggered during testing on `click`
789+
# versions < 8.2.1, due to (non-monkeypatch-able) deficiencies
790+
# in `click.testing.CliRunner`. Therefore, as an external source
791+
# of nondeterminism, exclude it from coverage.
792+
#
793+
# https://github.com/pallets/click/issues/2934
794+
return ''
776795

777796

778797
def toml_key(*parts: str) -> str:

tests/test_derivepassphrase_cli.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,16 +666,28 @@ class Parametrize(types.SimpleNamespace):
666666
),
667667
pytest.param(
668668
['--phrase', '--', 'sv'],
669-
'',
669+
'\n',
670670
'No passphrase was given',
671671
id='phrase-sv',
672672
),
673673
pytest.param(
674-
['--key'],
674+
['--phrase', '--', 'sv'],
675675
'',
676+
'No passphrase was given',
677+
id='phrase-sv-eof',
678+
),
679+
pytest.param(
680+
['--key'],
681+
'\n',
676682
'No SSH key was selected',
677683
id='key-sv',
678684
),
685+
pytest.param(
686+
['--key'],
687+
'',
688+
'No SSH key was selected',
689+
id='key-sv-eof',
690+
),
679691
],
680692
)
681693
CONFIG_EDITING_VIA_CONFIG_FLAG = pytest.mark.parametrize(
@@ -4406,7 +4418,7 @@ def driver(heading: str, items: list[str]) -> None:
44064418
"""
44074419
), 'expected clean exit'
44084420
result = runner.invoke(
4409-
driver, ['--heading='], input='', catch_exceptions=True
4421+
driver, ['--heading='], input='\n', catch_exceptions=True
44104422
)
44114423
assert result.error_exit(error=IndexError), (
44124424
'expected error exit and known error type'
@@ -4427,6 +4439,43 @@ def driver(heading: str, items: list[str]) -> None:
44274439
Your selection? (1-10, leave empty to abort):\x20
44284440
"""
44294441
), 'expected known output'
4442+
# click.testing.CliRunner on click < 8.2.1 incorrectly mocks the
4443+
# click prompting machinery, meaning that the mixed output will
4444+
# incorrectly contain a line break, contrary to what the
4445+
# documentation for click.prompt prescribes.
4446+
result = runner.invoke(
4447+
driver, ['--heading='], input='', catch_exceptions=True
4448+
)
4449+
assert result.error_exit(error=IndexError), (
4450+
'expected error exit and known error type'
4451+
)
4452+
assert result.stdout in {
4453+
"""\
4454+
[1] Egg and bacon
4455+
[2] Egg, sausage and bacon
4456+
[3] Egg and spam
4457+
[4] Egg, bacon and spam
4458+
[5] Egg, bacon, sausage and spam
4459+
[6] Spam, bacon, sausage and spam
4460+
[7] Spam, egg, spam, spam, bacon and spam
4461+
[8] Spam, spam, spam, egg and spam
4462+
[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
4463+
[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
4464+
Your selection? (1-10, leave empty to abort):\x20
4465+
""",
4466+
"""\
4467+
[1] Egg and bacon
4468+
[2] Egg, sausage and bacon
4469+
[3] Egg and spam
4470+
[4] Egg, bacon and spam
4471+
[5] Egg, bacon, sausage and spam
4472+
[6] Spam, bacon, sausage and spam
4473+
[7] Spam, egg, spam, spam, bacon and spam
4474+
[8] Spam, spam, spam, egg and spam
4475+
[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
4476+
[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
4477+
Your selection? (1-10, leave empty to abort): """,
4478+
}, 'expected known output'
44304479

44314480
def test_112_prompt_for_selection_single(self) -> None:
44324481
"""[`cli_helpers.prompt_for_selection`][] works in the "single" case."""
@@ -4459,7 +4508,7 @@ def driver(item: str, prompt: str) -> None:
44594508
result = runner.invoke(
44604509
driver,
44614510
['Will replace with spam, okay? (Please say "y" or "n".)'],
4462-
input='',
4511+
input='\n',
44634512
)
44644513
assert result.error_exit(error=IndexError), (
44654514
'expected error exit and known error type'
@@ -4472,6 +4521,29 @@ def driver(item: str, prompt: str) -> None:
44724521
Boo.
44734522
"""
44744523
), 'expected known output'
4524+
# click.testing.CliRunner on click < 8.2.1 incorrectly mocks the
4525+
# click prompting machinery, meaning that the mixed output will
4526+
# incorrectly contain a line break, contrary to what the
4527+
# documentation for click.prompt prescribes.
4528+
result = runner.invoke(
4529+
driver,
4530+
['Will replace with spam, okay? (Please say "y" or "n".)'],
4531+
input='',
4532+
)
4533+
assert result.error_exit(error=IndexError), (
4534+
'expected error exit and known error type'
4535+
)
4536+
assert result.stdout in {
4537+
"""\
4538+
[1] baked beans
4539+
Will replace with spam, okay? (Please say "y" or "n".):\x20
4540+
Boo.
4541+
""",
4542+
"""\
4543+
[1] baked beans
4544+
Will replace with spam, okay? (Please say "y" or "n".): Boo.
4545+
""",
4546+
}, 'expected known output'
44754547

44764548
def test_113_prompt_for_passphrase(
44774549
self,

0 commit comments

Comments
 (0)