Refactor the "all CLIs" command-line interface tests
Marco Ricci

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