Marco Ricci commited on 2025-08-29 20:00:38
Zeige 1 geänderte Dateien mit 81 Einfügungen und 197 Löschungen.
(This is part 9 of a series of refactorings for the test suite.) In the "all CLIs" tests for the command-line interface, factor out the common test operation for the "help text" and "version output" tests. Both sets of tests are identical for all command-line interface entry points, save for the command-line and the expected lines/data in the output. Furthermore, use a helper function to make the `KnownLineType` enum definition more pleasant to read.
| ... | ... |
@@ -27,33 +27,29 @@ class VersionOutputData(NamedTuple): |
| 27 | 27 |
features: dict[str, bool] |
| 28 | 28 |
|
| 29 | 29 |
|
| 30 |
+def _label_text(e: cli_messages.Label, /) -> str: |
|
| 31 |
+ return e.value.singular.rstrip(":")
|
|
| 32 |
+ |
|
| 33 |
+ |
|
| 30 | 34 |
class KnownLineType(str, enum.Enum): |
| 31 |
- SUPPORTED_FOREIGN_CONFS = cli_messages.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip( |
|
| 32 |
- ":" |
|
| 33 |
- ) |
|
| 34 |
- UNAVAILABLE_FOREIGN_CONFS = cli_messages.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip( |
|
| 35 |
- ":" |
|
| 36 |
- ) |
|
| 37 |
- SUPPORTED_SCHEMES = ( |
|
| 38 |
- cli_messages.Label.SUPPORTED_DERIVATION_SCHEMES.value.singular.rstrip( |
|
| 39 |
- ":" |
|
| 40 |
- ) |
|
| 41 |
- ) |
|
| 42 |
- UNAVAILABLE_SCHEMES = cli_messages.Label.UNAVAILABLE_DERIVATION_SCHEMES.value.singular.rstrip( |
|
| 43 |
- ":" |
|
| 35 |
+ SUPPORTED_FOREIGN_CONFS = _label_text( |
|
| 36 |
+ cli_messages.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS |
|
| 44 | 37 |
) |
| 45 |
- SUPPORTED_SUBCOMMANDS = ( |
|
| 46 |
- cli_messages.Label.SUPPORTED_SUBCOMMANDS.value.singular.rstrip(":")
|
|
| 38 |
+ UNAVAILABLE_FOREIGN_CONFS = _label_text( |
|
| 39 |
+ cli_messages.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS |
|
| 47 | 40 |
) |
| 48 |
- SUPPORTED_FEATURES = ( |
|
| 49 |
- cli_messages.Label.SUPPORTED_FEATURES.value.singular.rstrip(":")
|
|
| 41 |
+ SUPPORTED_SCHEMES = _label_text( |
|
| 42 |
+ cli_messages.Label.SUPPORTED_DERIVATION_SCHEMES |
|
| 50 | 43 |
) |
| 51 |
- UNAVAILABLE_FEATURES = ( |
|
| 52 |
- cli_messages.Label.UNAVAILABLE_FEATURES.value.singular.rstrip(":")
|
|
| 44 |
+ UNAVAILABLE_SCHEMES = _label_text( |
|
| 45 |
+ cli_messages.Label.UNAVAILABLE_DERIVATION_SCHEMES |
|
| 53 | 46 |
) |
| 54 |
- ENABLED_EXTRAS = ( |
|
| 55 |
- cli_messages.Label.ENABLED_PEP508_EXTRAS.value.singular.rstrip(":")
|
|
| 47 |
+ SUPPORTED_SUBCOMMANDS = _label_text( |
|
| 48 |
+ cli_messages.Label.SUPPORTED_SUBCOMMANDS |
|
| 56 | 49 |
) |
| 50 |
+ SUPPORTED_FEATURES = _label_text(cli_messages.Label.SUPPORTED_FEATURES) |
|
| 51 |
+ UNAVAILABLE_FEATURES = _label_text(cli_messages.Label.UNAVAILABLE_FEATURES) |
|
| 52 |
+ ENABLED_EXTRAS = _label_text(cli_messages.Label.ENABLED_PEP508_EXTRAS) |
|
| 57 | 53 |
|
| 58 | 54 |
|
| 59 | 55 |
class Parametrize(types.SimpleNamespace): |
| ... | ... |
@@ -109,6 +105,34 @@ class Parametrize(types.SimpleNamespace): |
| 109 | 105 |
), |
| 110 | 106 |
], |
| 111 | 107 |
) |
| 108 |
+ HELP_OUTPUT_COMMAND_LINE = pytest.mark.parametrize( |
|
| 109 |
+ ["command_line", "expected_lines"], |
|
| 110 |
+ [ |
|
| 111 |
+ pytest.param( |
|
| 112 |
+ [], |
|
| 113 |
+ ["currently implemented subcommands"], |
|
| 114 |
+ id="derivepassphrase", |
|
| 115 |
+ ), |
|
| 116 |
+ pytest.param( |
|
| 117 |
+ ["export"], |
|
| 118 |
+ ["only available subcommand"], |
|
| 119 |
+ id="derivepassphrase-export", |
|
| 120 |
+ ), |
|
| 121 |
+ pytest.param( |
|
| 122 |
+ ["export", "vault"], |
|
| 123 |
+ ["Export a vault-native configuration"], |
|
| 124 |
+ id="derivepassphrase-export-vault", |
|
| 125 |
+ ), |
|
| 126 |
+ pytest.param( |
|
| 127 |
+ ["vault"], |
|
| 128 |
+ [ |
|
| 129 |
+ "Passphrase generation:", |
|
| 130 |
+ "Use $VISUAL or $EDITOR to configure", |
|
| 131 |
+ ], |
|
| 132 |
+ id="derivepassphrase-vault", |
|
| 133 |
+ ), |
|
| 134 |
+ ], |
|
| 135 |
+ ) |
|
| 112 | 136 |
COLORFUL_COMMAND_INPUT = pytest.mark.parametrize( |
| 113 | 137 |
["command_line", "input"], |
| 114 | 138 |
[ |
| ... | ... |
@@ -386,37 +410,13 @@ class TestHelpOutput: |
| 386 | 410 |
|
| 387 | 411 |
# TODO(the-13th-letter): Do we actually need this? What should we |
| 388 | 412 |
# check for? |
| 389 |
- def test_help_output(self) -> None: |
|
| 390 |
- """The top-level help text mentions subcommands. |
|
| 391 |
- |
|
| 392 |
- TODO: Do we actually need this? What should we check for? |
|
| 393 |
- |
|
| 394 |
- """ |
|
| 395 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 396 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 397 |
- # with-statements. |
|
| 398 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 399 |
- with contextlib.ExitStack() as stack: |
|
| 400 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 401 |
- stack.enter_context( |
|
| 402 |
- pytest_machinery.isolated_config( |
|
| 403 |
- monkeypatch=monkeypatch, |
|
| 404 |
- runner=runner, |
|
| 405 |
- ) |
|
| 406 |
- ) |
|
| 407 |
- result = runner.invoke( |
|
| 408 |
- cli.derivepassphrase, ["--help"], catch_exceptions=False |
|
| 409 |
- ) |
|
| 410 |
- assert result.clean_exit( |
|
| 411 |
- empty_stderr=True, output="currently implemented subcommands" |
|
| 412 |
- ), "expected clean exit, and known help text" |
|
| 413 |
- |
|
| 414 |
- # TODO(the-13th-letter): Do we actually need this? What should we |
|
| 415 |
- # check for? |
|
| 416 |
- def test_help_output_export( |
|
| 413 |
+ @Parametrize.HELP_OUTPUT_COMMAND_LINE |
|
| 414 |
+ def test_help_output( |
|
| 417 | 415 |
self, |
| 416 |
+ command_line: list[str], |
|
| 417 |
+ expected_lines: list[str], |
|
| 418 | 418 |
) -> None: |
| 419 |
- """The "export" subcommand help text mentions subcommands. |
|
| 419 |
+ """The respective help text contains certain expected phrases. |
|
| 420 | 420 |
|
| 421 | 421 |
TODO: Do we actually need this? What should we check for? |
| 422 | 422 |
|
| ... | ... |
@@ -435,77 +435,13 @@ class TestHelpOutput: |
| 435 | 435 |
) |
| 436 | 436 |
result = runner.invoke( |
| 437 | 437 |
cli.derivepassphrase, |
| 438 |
- ["export", "--help"], |
|
| 438 |
+ [*command_line, "--help"], |
|
| 439 | 439 |
catch_exceptions=False, |
| 440 | 440 |
) |
| 441 |
- assert result.clean_exit( |
|
| 442 |
- empty_stderr=True, output="only available subcommand" |
|
| 443 |
- ), "expected clean exit, and known help text" |
|
| 444 |
- |
|
| 445 |
- # TODO(the-13th-letter): Do we actually need this? What should we |
|
| 446 |
- # check for? |
|
| 447 |
- def test_help_output_export_vault( |
|
| 448 |
- self, |
|
| 449 |
- ) -> None: |
|
| 450 |
- """The "export vault" subcommand help text has known content. |
|
| 451 |
- |
|
| 452 |
- TODO: Do we actually need this? What should we check for? |
|
| 453 |
- |
|
| 454 |
- """ |
|
| 455 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 456 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 457 |
- # with-statements. |
|
| 458 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 459 |
- with contextlib.ExitStack() as stack: |
|
| 460 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 461 |
- stack.enter_context( |
|
| 462 |
- pytest_machinery.isolated_config( |
|
| 463 |
- monkeypatch=monkeypatch, |
|
| 464 |
- runner=runner, |
|
| 465 |
- ) |
|
| 441 |
+ for line in expected_lines: |
|
| 442 |
+ assert result.clean_exit(empty_stderr=True, output=line), ( |
|
| 443 |
+ "expected clean exit, and known help text" |
|
| 466 | 444 |
) |
| 467 |
- result = runner.invoke( |
|
| 468 |
- cli.derivepassphrase, |
|
| 469 |
- ["export", "vault", "--help"], |
|
| 470 |
- catch_exceptions=False, |
|
| 471 |
- ) |
|
| 472 |
- assert result.clean_exit( |
|
| 473 |
- empty_stderr=True, output="Export a vault-native configuration" |
|
| 474 |
- ), "expected clean exit, and known help text" |
|
| 475 |
- |
|
| 476 |
- # TODO(the-13th-letter): Do we actually need this? What should we |
|
| 477 |
- # check for? |
|
| 478 |
- def test_help_output_vault( |
|
| 479 |
- self, |
|
| 480 |
- ) -> None: |
|
| 481 |
- """The "vault" subcommand help text has known content. |
|
| 482 |
- |
|
| 483 |
- TODO: Do we actually need this? What should we check for? |
|
| 484 |
- |
|
| 485 |
- """ |
|
| 486 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 487 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 488 |
- # with-statements. |
|
| 489 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 490 |
- with contextlib.ExitStack() as stack: |
|
| 491 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 492 |
- stack.enter_context( |
|
| 493 |
- pytest_machinery.isolated_config( |
|
| 494 |
- monkeypatch=monkeypatch, |
|
| 495 |
- runner=runner, |
|
| 496 |
- ) |
|
| 497 |
- ) |
|
| 498 |
- result = runner.invoke( |
|
| 499 |
- cli.derivepassphrase, |
|
| 500 |
- ["vault", "--help"], |
|
| 501 |
- catch_exceptions=False, |
|
| 502 |
- ) |
|
| 503 |
- assert result.clean_exit( |
|
| 504 |
- empty_stderr=True, output="Passphrase generation:\n" |
|
| 505 |
- ), "expected clean exit, and option groups in help text" |
|
| 506 |
- assert result.clean_exit( |
|
| 507 |
- empty_stderr=True, output="Use $VISUAL or $EDITOR to configure" |
|
| 508 |
- ), "expected clean exit, and option group epilog in help text" |
|
| 509 | 445 |
|
| 510 | 446 |
@Parametrize.COMMAND_NON_EAGER_ARGUMENTS |
| 511 | 447 |
@Parametrize.EAGER_ARGUMENTS |
| ... | ... |
@@ -585,23 +521,10 @@ class TestHelpOutput: |
| 585 | 521 |
class TestVersionOutput: |
| 586 | 522 |
"""Tests for all command-line interfaces' `--version` output.""" |
| 587 | 523 |
|
| 588 |
- def test_derivepassphrase_version_option_output( |
|
| 524 |
+ def _test( |
|
| 589 | 525 |
self, |
| 590 |
- ) -> None: |
|
| 591 |
- """The version output states supported features. |
|
| 592 |
- |
|
| 593 |
- The version output is parsed using [`parse_version_output`][]. |
|
| 594 |
- Format examples can be found in |
|
| 595 |
- [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the |
|
| 596 |
- top-level `derivepassphrase` command, the output should contain |
|
| 597 |
- the known and supported derivation schemes, and a list of |
|
| 598 |
- subcommands. |
|
| 599 |
- |
|
| 600 |
- As a side effect, [`parse_version_output`][] guarantees that the |
|
| 601 |
- first line contains both the correct program name as well as the |
|
| 602 |
- correct program version number. |
|
| 603 |
- |
|
| 604 |
- """ |
|
| 526 |
+ command_line: list[str], |
|
| 527 |
+ ) -> VersionOutputData: |
|
| 605 | 528 |
runner = machinery.CliRunner(mix_stderr=False) |
| 606 | 529 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 607 | 530 |
# with-statements. |
| ... | ... |
@@ -616,12 +539,31 @@ class TestVersionOutput: |
| 616 | 539 |
) |
| 617 | 540 |
result = runner.invoke( |
| 618 | 541 |
cli.derivepassphrase, |
| 619 |
- ["--version"], |
|
| 542 |
+ [*command_line, "--version"], |
|
| 620 | 543 |
catch_exceptions=False, |
| 621 | 544 |
) |
| 622 | 545 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
| 623 | 546 |
assert result.stdout.strip(), "expected version output" |
| 624 |
- version_data = parse_version_output(result.stdout) |
|
| 547 |
+ return parse_version_output(result.stdout) |
|
| 548 |
+ |
|
| 549 |
+ def test_derivepassphrase_version_option_output( |
|
| 550 |
+ self, |
|
| 551 |
+ ) -> None: |
|
| 552 |
+ """The version output states supported features. |
|
| 553 |
+ |
|
| 554 |
+ The version output is parsed using [`parse_version_output`][]. |
|
| 555 |
+ Format examples can be found in |
|
| 556 |
+ [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the |
|
| 557 |
+ top-level `derivepassphrase` command, the output should contain |
|
| 558 |
+ the known and supported derivation schemes, and a list of |
|
| 559 |
+ subcommands. |
|
| 560 |
+ |
|
| 561 |
+ As a side effect, [`parse_version_output`][] guarantees that the |
|
| 562 |
+ first line contains both the correct program name as well as the |
|
| 563 |
+ correct program version number. |
|
| 564 |
+ |
|
| 565 |
+ """ |
|
| 566 |
+ version_data = self._test([]) |
|
| 625 | 567 |
actually_known_schemes = dict.fromkeys(_types.DerivationScheme, True) |
| 626 | 568 |
subcommands = set(_types.Subcommand) |
| 627 | 569 |
assert version_data.derivation_schemes == actually_known_schemes |
| ... | ... |
@@ -647,26 +589,7 @@ class TestVersionOutput: |
| 647 | 589 |
correct program version number. |
| 648 | 590 |
|
| 649 | 591 |
""" |
| 650 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 651 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 652 |
- # with-statements. |
|
| 653 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 654 |
- with contextlib.ExitStack() as stack: |
|
| 655 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 656 |
- stack.enter_context( |
|
| 657 |
- pytest_machinery.isolated_config( |
|
| 658 |
- monkeypatch=monkeypatch, |
|
| 659 |
- runner=runner, |
|
| 660 |
- ) |
|
| 661 |
- ) |
|
| 662 |
- result = runner.invoke( |
|
| 663 |
- cli.derivepassphrase, |
|
| 664 |
- ["export", "--version"], |
|
| 665 |
- catch_exceptions=False, |
|
| 666 |
- ) |
|
| 667 |
- assert result.clean_exit(empty_stderr=True), "expected clean exit" |
|
| 668 |
- assert result.stdout.strip(), "expected version output" |
|
| 669 |
- version_data = parse_version_output(result.stdout) |
|
| 592 |
+ version_data = self._test(["export"]) |
|
| 670 | 593 |
actually_known_formats: dict[str, bool] = {
|
| 671 | 594 |
_types.ForeignConfigurationFormat.VAULT_STOREROOM: False, |
| 672 | 595 |
_types.ForeignConfigurationFormat.VAULT_V02: False, |
| ... | ... |
@@ -699,26 +622,7 @@ class TestVersionOutput: |
| 699 | 622 |
correct program version number. |
| 700 | 623 |
|
| 701 | 624 |
""" |
| 702 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 703 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 704 |
- # with-statements. |
|
| 705 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 706 |
- with contextlib.ExitStack() as stack: |
|
| 707 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 708 |
- stack.enter_context( |
|
| 709 |
- pytest_machinery.isolated_config( |
|
| 710 |
- monkeypatch=monkeypatch, |
|
| 711 |
- runner=runner, |
|
| 712 |
- ) |
|
| 713 |
- ) |
|
| 714 |
- result = runner.invoke( |
|
| 715 |
- cli.derivepassphrase, |
|
| 716 |
- ["export", "vault", "--version"], |
|
| 717 |
- catch_exceptions=False, |
|
| 718 |
- ) |
|
| 719 |
- assert result.clean_exit(empty_stderr=True), "expected clean exit" |
|
| 720 |
- assert result.stdout.strip(), "expected version output" |
|
| 721 |
- version_data = parse_version_output(result.stdout) |
|
| 625 |
+ version_data = self._test(["export", "vault"]) |
|
| 722 | 626 |
actually_known_formats: dict[str, bool] = {}
|
| 723 | 627 |
actually_enabled_extras: set[str] = set() |
| 724 | 628 |
with contextlib.suppress(ModuleNotFoundError): |
| ... | ... |
@@ -758,27 +662,7 @@ class TestVersionOutput: |
| 758 | 662 |
correct program version number. |
| 759 | 663 |
|
| 760 | 664 |
""" |
| 761 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 762 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 763 |
- # with-statements. |
|
| 764 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 765 |
- with contextlib.ExitStack() as stack: |
|
| 766 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 767 |
- stack.enter_context( |
|
| 768 |
- pytest_machinery.isolated_config( |
|
| 769 |
- monkeypatch=monkeypatch, |
|
| 770 |
- runner=runner, |
|
| 771 |
- ) |
|
| 772 |
- ) |
|
| 773 |
- result = runner.invoke( |
|
| 774 |
- cli.derivepassphrase, |
|
| 775 |
- ["vault", "--version"], |
|
| 776 |
- catch_exceptions=False, |
|
| 777 |
- ) |
|
| 778 |
- assert result.clean_exit(empty_stderr=True), "expected clean exit" |
|
| 779 |
- assert result.stdout.strip(), "expected version output" |
|
| 780 |
- version_data = parse_version_output(result.stdout) |
|
| 781 |
- |
|
| 665 |
+ version_data = self._test(["vault"]) |
|
| 782 | 666 |
ssh_key_supported = True |
| 783 | 667 |
|
| 784 | 668 |
def react_to_notimplementederror( |
| 785 | 669 |