Coverage for tests / test_derivepassphrase_cli / test_all_cli.py: 100.000%
201 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5"""Tests for the `derivepassphrase` command-line interface: all subcommands.
7This includes tests for functionality or options common to all
8subcommands.
10"""
12from __future__ import annotations
14import contextlib
15import enum
16import re
17import types
18from typing import TYPE_CHECKING
20import exceptiongroup
21import pytest
22from typing_extensions import NamedTuple
24from derivepassphrase import _types, cli, ssh_agent
25from derivepassphrase._internals import cli_messages
26from tests import machinery
27from tests.machinery import pytest as pytest_machinery
29if TYPE_CHECKING:
30 from collections.abc import Generator
33class VersionOutputData(NamedTuple):
34 derivation_schemes: dict[str, bool]
35 foreign_configuration_formats: dict[str, bool]
36 extras: frozenset[str]
37 subcommands: frozenset[str]
38 features: dict[str, bool]
39 ssh_agent_socket_providers: dict[str, bool]
42def _label_text(e: cli_messages.Label, /) -> str:
43 return e.value.singular.rstrip(":")
46class KnownLineType(str, enum.Enum):
47 SUPPORTED_FOREIGN_CONFS = _label_text(
48 cli_messages.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS
49 )
50 UNAVAILABLE_FOREIGN_CONFS = _label_text(
51 cli_messages.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS
52 )
53 SUPPORTED_SCHEMES = _label_text(
54 cli_messages.Label.SUPPORTED_DERIVATION_SCHEMES
55 )
56 UNAVAILABLE_SCHEMES = _label_text(
57 cli_messages.Label.UNAVAILABLE_DERIVATION_SCHEMES
58 )
59 SUPPORTED_SUBCOMMANDS = _label_text(
60 cli_messages.Label.SUPPORTED_SUBCOMMANDS
61 )
62 SUPPORTED_FEATURES = _label_text(cli_messages.Label.SUPPORTED_FEATURES)
63 UNAVAILABLE_FEATURES = _label_text(cli_messages.Label.UNAVAILABLE_FEATURES)
64 SUPPORTED_SSH_AGENT_SOCKET_PROVIDERS = _label_text(
65 cli_messages.Label.SUPPORTED_SSH_AGENT_SOCKET_PROVIDERS
66 )
67 UNAVAILABLE_SSH_AGENT_SOCKET_PROVIDERS = _label_text(
68 cli_messages.Label.UNAVAILABLE_SSH_AGENT_SOCKET_PROVIDERS
69 )
70 ENABLED_EXTRAS = _label_text(cli_messages.Label.ENABLED_PEP508_EXTRAS)
73class Parametrize(types.SimpleNamespace):
74 """Common test parametrizations."""
76 EAGER_ARGUMENTS = pytest.mark.parametrize(
77 "arguments",
78 [["--help"], ["--version"]],
79 ids=["help", "version"],
80 )
81 COMMAND_NON_EAGER_ARGUMENTS = pytest.mark.parametrize(
82 ["command", "non_eager_arguments"],
83 [
84 pytest.param(
85 [],
86 [],
87 id="top-nothing",
88 ),
89 pytest.param(
90 [],
91 ["export"],
92 id="top-export",
93 ),
94 pytest.param(
95 ["export"],
96 [],
97 id="export-nothing",
98 ),
99 pytest.param(
100 ["export"],
101 ["vault"],
102 id="export-vault",
103 ),
104 pytest.param(
105 ["export", "vault"],
106 [],
107 id="export-vault-nothing",
108 ),
109 pytest.param(
110 ["export", "vault"],
111 ["--format", "this-format-doesnt-exist"],
112 id="export-vault-args",
113 ),
114 pytest.param(
115 ["vault"],
116 [],
117 id="vault-nothing",
118 ),
119 pytest.param(
120 ["vault"],
121 ["--export", "./"],
122 id="vault-args",
123 ),
124 ],
125 )
126 HELP_OUTPUT_COMMAND_LINE = pytest.mark.parametrize(
127 ["command_line", "expected_lines"],
128 [
129 pytest.param(
130 [],
131 ["currently implemented subcommands"],
132 id="derivepassphrase",
133 ),
134 pytest.param(
135 ["export"],
136 ["only available subcommand"],
137 id="derivepassphrase-export",
138 ),
139 pytest.param(
140 ["export", "vault"],
141 ["Export a vault-native configuration"],
142 id="derivepassphrase-export-vault",
143 ),
144 pytest.param(
145 ["vault"],
146 [
147 "Passphrase generation:",
148 "Use $VISUAL or $EDITOR to configure",
149 ],
150 id="derivepassphrase-vault",
151 ),
152 ],
153 )
154 COLORFUL_COMMAND_INPUT = pytest.mark.parametrize(
155 ["command_line", "input"],
156 [
157 (
158 ["vault", "--import", "-"],
159 '{"services": {"": {"length": 20}}}',
160 ),
161 ],
162 ids=["cmd"],
163 )
164 ISATTY = pytest.mark.parametrize(
165 "isatty",
166 [False, True],
167 ids=["notty", "tty"],
168 )
169 MASK_PROG_NAME = pytest.mark.parametrize(
170 "mask_prog_name",
171 [False, True],
172 ids=["clear_prog_name", "masked_prog_name"],
173 )
174 MASK_VERSION = pytest.mark.parametrize(
175 "mask_version", [False, True], ids=["clear_version", "masked_version"]
176 )
177 VERSION_OUTPUT_DATA = pytest.mark.parametrize(
178 ["version_output", "prog_name", "version", "expected_parse"],
179 [
180 pytest.param(
181 """\
182derivepassphrase 0.4.0
183Using cryptography 44.0.0
185Supported foreign configuration formats: vault storeroom, vault v0.2,
186 vault v0.3.
187PEP 508 extras: export.
188""",
189 "derivepassphrase",
190 "0.4.0",
191 VersionOutputData(
192 derivation_schemes={},
193 foreign_configuration_formats={
194 "vault storeroom": True,
195 "vault v0.2": True,
196 "vault v0.3": True,
197 },
198 subcommands=frozenset(),
199 features={},
200 extras=frozenset({"export"}),
201 ssh_agent_socket_providers={},
202 ),
203 id="derivepassphrase-0.4.0-export",
204 ),
205 pytest.param(
206 """\
207derivepassphrase 0.5
209Supported derivation schemes: vault.
210Known foreign configuration formats: vault storeroom, vault v0.2, vault v0.3.
211Supported subcommands: export, vault.
212No PEP 508 extras are active.
213""",
214 "derivepassphrase",
215 "0.5",
216 VersionOutputData(
217 derivation_schemes={"vault": True},
218 foreign_configuration_formats={
219 "vault storeroom": False,
220 "vault v0.2": False,
221 "vault v0.3": False,
222 },
223 subcommands=frozenset({"export", "vault"}),
224 features={},
225 extras=frozenset({}),
226 ssh_agent_socket_providers={},
227 ),
228 id="derivepassphrase-0.5-plain",
229 ),
230 pytest.param(
231 """\
235inventpassphrase -1.3
236Using not-a-library 7.12
237Copyright 2025 Nobody. All rights reserved.
239Supported derivation schemes: nonsense.
240Known derivation schemes: divination, /dev/random,
241 geiger counter,
242 crossword solver.
243Supported foreign configuration formats: derivepassphrase, nonsense.
244Known foreign configuration formats: divination v3.141592,
245 /dev/random.
246Supported subcommands: delete-all-files, dump-core.
247Supported features: delete-while-open.
248Known features: backups-are-nice-to-have.
249Supported SSH agent socket providers: agents-of-shield.
250Known SSH agent socket providers: agent-smith.
251PEP 508 extras: annoying-popups, delete-all-files,
252 dump-core-depending-on-the-phase-of-the-moon.
256""",
257 "inventpassphrase",
258 "-1.3",
259 VersionOutputData(
260 derivation_schemes={
261 "nonsense": True,
262 "divination": False,
263 "/dev/random": False,
264 "geiger counter": False,
265 "crossword solver": False,
266 },
267 foreign_configuration_formats={
268 "derivepassphrase": True,
269 "nonsense": True,
270 "divination v3.141592": False,
271 "/dev/random": False,
272 },
273 subcommands=frozenset({"delete-all-files", "dump-core"}),
274 features={
275 "delete-while-open": True,
276 "backups-are-nice-to-have": False,
277 },
278 extras=frozenset({
279 "annoying-popups",
280 "delete-all-files",
281 "dump-core-depending-on-the-phase-of-the-moon",
282 }),
283 ssh_agent_socket_providers={
284 "agents-of-shield": True,
285 "agent-smith": False,
286 },
287 ),
288 id="inventpassphrase",
289 ),
290 pytest.param(
291 """\
292derivepassphrase 1.0
293Using wishful-thinking 2.0.
295Supported derivation schemes: spectre ({aliases!s} master-password, mpw),
296 vault.
297Supported subcommands: export, spectre ({aliases!s} master-password, mpw),
298 vault.
299""".format(
300 aliases=cli_messages.TranslatedString(
301 cli_messages.Label.FEATURE_ITEM_ALIASES
302 )
303 ),
304 "derivepassphrase",
305 "1.0",
306 VersionOutputData(
307 derivation_schemes={
308 "master-password": True,
309 "mpw": True,
310 "spectre": True,
311 "vault": True,
312 },
313 foreign_configuration_formats={},
314 subcommands=frozenset({
315 "export",
316 "master-password",
317 "mpw",
318 "spectre",
319 "vault",
320 }),
321 features={},
322 extras=frozenset(),
323 ssh_agent_socket_providers={},
324 ),
325 id="aliases",
326 ),
327 ],
328 )
329 """Sample data for [`parse_version_output`][]."""
332def tokenize_version_output_item_listing(
333 line: str,
334 /,
335 *,
336 is_alias_annotation: bool = False,
337) -> Generator[str, None, None]:
338 """Yield the next feature in a `--version` feature listing.
340 This is a regular expression-based parser (alluded to in
341 [`parse_version_output`][]) that yields the next item name or alias
342 it encounters. (The output is indistinguishable for those two
343 types.)
345 We assume that continuation lines have already been reversed, i.e.,
346 that the whole input is on a single line. We further assume that
347 the listing header has been removed, i.e., we are only processing
348 the raw list items.
350 Args:
351 line:
352 The input line, normalized as explained above.
353 is_alias_annotation:
354 If true, then the input line is contents of an alias
355 listing, and itself does not support aliases. Otherwise,
356 aliases are supported.
358 Yields:
359 The next item name or alias. There is no way to distinguish
360 these cases based on output alone.
362 """
363 chunk_re = re.compile( 1cfbea
364 r"""
365 # the item name
366 (?P<name>[^,()]+)
367 # whitespace
368 (?:[ ]*)
369 # the terminator
370 (?:,[ ]*|$)
371 """
372 if is_alias_annotation
373 else r"""
374 # the item name
375 (?P<name>[^,()]+)
376 # alias list
377 (?:[ ]+
378 # alias marker
379 \(
380 {aliases_marker!s}
381 [ ]+
382 # the alias entries
383 (?P<alias_list>[^()]+)
384 \)
385 )?
386 # the terminator
387 (?:,[ ]*|\.$)
388 """.format(
389 aliases_marker=cli_messages.TranslatedString(
390 cli_messages.Label.FEATURE_ITEM_ALIASES
391 )
392 ),
393 re.VERBOSE,
394 )
395 rest = line.strip() 1cfbea
396 while (match := chunk_re.match(rest)) is not None: 1cfbea
397 name = match.group("name") 1cfbea
398 assert name, "item listing tokenizer is inconsistent" 1cfbea
399 assert name.strip() == name, "item listing tokenizer is inconsistent" 1cfbea
400 yield name 1cfbea
401 if not is_alias_annotation and match.group("alias_list"): 1cfbea
402 alias_list = match.group("alias_list") 1ca
403 assert alias_list, "item listing tokenizer is inconsistent" 1ca
404 assert alias_list.strip(), "item listing tokenizer is inconsistent" 1ca
405 assert alias_list.lstrip() == alias_list, ( 1ca
406 "item listing tokenizer is inconsistent"
407 )
408 yield from tokenize_version_output_item_listing( 1ca
409 alias_list.strip(), is_alias_annotation=True
410 )
411 rest = rest.removeprefix(match.group(0)) 1cfbea
412 if rest: # pragma: no cover [defensive] 1cfbea
413 msg = f"Trailing unparsable junk on {line!r}: {rest!r}"
414 raise ValueError(msg)
417def parse_version_output( # noqa: C901
418 version_output: str,
419 /,
420 *,
421 prog_name: str | None = cli_messages.PROG_NAME,
422 version: str | None = cli_messages.VERSION,
423) -> VersionOutputData:
424 r"""Parse the output of the `--version` option.
426 The version output contains two paragraphs. The first paragraph
427 details the version number, and the version number of any major
428 libraries in use. The second paragraph details known and supported
429 passphrase derivation schemes, foreign configuration formats,
430 subcommands, SSH agent socket providers and PEP 508 package extras.
432 For the schemes, formats and socket providers, there is
433 a "supported" line for supported items, and a "known" line for known
434 but currently unsupported items (usually because of missing
435 dependencies), either of which may be empty and thus omitted. For
436 extras, only active items are shown, and there is a separate message
437 for the "no extras active" case. Items may be followed by a list of
438 aliases, explicitly marked as such. Item lists may be spilled
439 across multiple lines, but only at item boundaries. (The alias list
440 counts as part of the same item.) The continuation lines are then
441 indented.
443 The list of aliases is formatted as `<name> (aliases: <alias1>,
444 <alias2>)`. Only one level of aliases is supported, and neither
445 `<name>` nor `<alias>` must contain parentheses. (Brackets and
446 braces are discouraged, but not expressly forbidden.)
448 Args:
449 version_output:
450 The version output text to parse.
451 prog_name:
452 The program name to assert, defaulting to the true program
453 name, `derivepassphrase`. Set to `None` to disable this
454 check.
455 version:
456 The program version to assert, defaulting to the true
457 current version of `derivepassphrase`. Set to `None` to
458 disable this check.
460 Examples:
461 See [`Parametrize.VERSION_OUTPUT_DATA`][].
463 See also:
464 * [`tokenize_version_output_item_listing`][]
466 """
467 paragraphs: list[list[str]] = [] 1cfbea
468 paragraph: list[str] = [] 1cfbea
469 for line in version_output.splitlines(keepends=False): 1cfbea
470 if not line.strip(): 1cfbea
471 if paragraph: 1cfbea
472 paragraphs.append(paragraph.copy()) 1cfbea
473 paragraph.clear() 1cfbea
474 elif paragraph and line.lstrip() != line: 1cfbea
475 paragraph[-1] = f"{paragraph[-1]} {line.lstrip()}" 1cbea
476 else:
477 paragraph.append(line) 1cfbea
478 if paragraph: # pragma: no branch 1cfbea
479 paragraphs.append(paragraph.copy()) 1cfbea
480 paragraph.clear() 1cfbea
481 assert paragraphs, ( 1cfbea
482 f"expected at least one paragraph of version output: {paragraphs!r}"
483 )
484 assert prog_name is None or prog_name in paragraphs[0][0], ( 1cfbea
485 f"first version output line should mention "
486 f"{prog_name}: {paragraphs[0][0]!r}"
487 )
488 assert version is None or version in paragraphs[0][0], ( 1cfbea
489 f"first version output line should mention the version number "
490 f"{version}: {paragraphs[0][0]!r}"
491 )
492 schemes: dict[str, bool] = {} 1cfbea
493 formats: dict[str, bool] = {} 1cfbea
494 subcommands: set[str] = set() 1cfbea
495 extras: set[str] = set() 1cfbea
496 features: dict[str, bool] = {} 1cfbea
497 ssh_agent_socket_providers: dict[str, bool] = {} 1cfbea
498 if len(paragraphs) < 2: # pragma: no cover 1cfbea
499 return VersionOutputData(
500 derivation_schemes=schemes,
501 foreign_configuration_formats=formats,
502 subcommands=frozenset(subcommands),
503 extras=frozenset(extras),
504 features=features,
505 ssh_agent_socket_providers=ssh_agent_socket_providers,
506 )
507 for line in paragraphs[1]: 1cfbea
508 line_type, _, value = line.partition(":") 1cfbea
509 if line_type == line: 1cfbea
510 continue 1c
511 for item_ in tokenize_version_output_item_listing(value): 1cfbea
512 item = item_.strip() 1cfbea
513 if line_type == KnownLineType.SUPPORTED_FOREIGN_CONFS: 1cfbea
514 formats[item] = True 1cb
515 elif line_type == KnownLineType.UNAVAILABLE_FOREIGN_CONFS: 1cfbea
516 formats[item] = False 1cbe
517 elif line_type == KnownLineType.SUPPORTED_SCHEMES: 1cfbea
518 schemes[item] = True 1cf
519 elif line_type == KnownLineType.UNAVAILABLE_SCHEMES: 1cfbea
520 schemes[item] = False 1c
521 elif line_type == KnownLineType.SUPPORTED_SUBCOMMANDS: 1cfbea
522 subcommands.add(item) 1cfe
523 elif line_type == KnownLineType.ENABLED_EXTRAS: 1cba
524 extras.add(item) 1cb
525 elif line_type == KnownLineType.SUPPORTED_FEATURES: 1ca
526 features[item] = True 1ca
527 elif line_type == KnownLineType.UNAVAILABLE_FEATURES: 1ca
528 features[item] = False 1c
529 elif ( 1ca
530 line_type == KnownLineType.SUPPORTED_SSH_AGENT_SOCKET_PROVIDERS
531 ):
532 ssh_agent_socket_providers[item] = True 1ca
533 elif ( 1ca
534 line_type
535 == KnownLineType.UNAVAILABLE_SSH_AGENT_SOCKET_PROVIDERS
536 ):
537 ssh_agent_socket_providers[item] = False 1ca
538 else:
539 raise AssertionError( # noqa: TRY003
540 f"Unknown version info line type: {line_type!r}" # noqa: EM102
541 )
542 return VersionOutputData( 1cfbea
543 derivation_schemes=schemes,
544 foreign_configuration_formats=formats,
545 subcommands=frozenset(subcommands),
546 extras=frozenset(extras),
547 features=features,
548 ssh_agent_socket_providers=ssh_agent_socket_providers,
549 )
552class Test001VersionOutputParser:
553 """Tests for the `--version` output parser."""
555 @Parametrize.MASK_PROG_NAME
556 @Parametrize.MASK_VERSION
557 @Parametrize.VERSION_OUTPUT_DATA
558 def test_parse_version_output(
559 self,
560 version_output: str,
561 prog_name: str | None,
562 version: str | None,
563 mask_prog_name: bool,
564 mask_version: bool,
565 expected_parse: VersionOutputData,
566 ) -> None:
567 """The parsing machinery for expected version output data works."""
568 prog_name = None if mask_prog_name else prog_name 1c
569 version = None if mask_version else version 1c
570 assert ( 1c
571 parse_version_output(
572 version_output, prog_name=prog_name, version=version
573 )
574 == expected_parse
575 )
578class TestHelpOutput:
579 """Tests for all command-line interfaces' `--help` output."""
581 # TODO(the-13th-letter): Do we actually need this? What should we
582 # check for?
583 @Parametrize.HELP_OUTPUT_COMMAND_LINE
584 def test_help_output(
585 self,
586 command_line: list[str],
587 expected_lines: list[str],
588 ) -> None:
589 """The respective help text contains certain expected phrases.
591 TODO: Do we actually need this? What should we check for?
593 """
594 runner = machinery.CliRunner(mix_stderr=False) 1h
595 # TODO(the-13th-letter): Rewrite using parenthesized
596 # with-statements.
597 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
598 with contextlib.ExitStack() as stack: 1h
599 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1h
600 stack.enter_context( 1h
601 pytest_machinery.isolated_config(
602 monkeypatch=monkeypatch,
603 runner=runner,
604 )
605 )
606 result = runner.invoke( 1h
607 cli.derivepassphrase,
608 [*command_line, "--help"],
609 catch_exceptions=False,
610 )
611 for line in expected_lines: 1h
612 assert result.clean_exit(empty_stderr=True, output=line), ( 1h
613 "expected clean exit, and known help text"
614 )
616 @Parametrize.COMMAND_NON_EAGER_ARGUMENTS
617 @Parametrize.EAGER_ARGUMENTS
618 def test_eager_options(
619 self,
620 command: list[str],
621 arguments: list[str],
622 non_eager_arguments: list[str],
623 ) -> None:
624 """Eager options terminate option and argument processing."""
625 runner = machinery.CliRunner(mix_stderr=False) 1i
626 # TODO(the-13th-letter): Rewrite using parenthesized
627 # with-statements.
628 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
629 with contextlib.ExitStack() as stack: 1i
630 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1i
631 stack.enter_context( 1i
632 pytest_machinery.isolated_config(
633 monkeypatch=monkeypatch,
634 runner=runner,
635 )
636 )
637 result = runner.invoke( 1i
638 cli.derivepassphrase,
639 [*command, *arguments, *non_eager_arguments],
640 catch_exceptions=False,
641 )
642 assert result.clean_exit(empty_stderr=True), "expected clean exit" 1i
644 @Parametrize.ISATTY
645 @Parametrize.COLORFUL_COMMAND_INPUT
646 def test_automatic_color_mode(
647 self,
648 isatty: bool,
649 command_line: list[str],
650 input: str | None,
651 ) -> None:
652 """Auto-detect if color should be used.
654 (The answer currently is always no. See the
655 [`conventional-configurable-text-styling` wishlist
656 entry][WISHLIST_ENTRY].)
658 [WISHLIST_ENTRY]: https://the13thletter.info/derivepassphrase/0.x/wishlist/conventional-configurable-text-styling/
660 """
661 color = False 1g
662 runner = machinery.CliRunner(mix_stderr=False) 1g
663 # TODO(the-13th-letter): Rewrite using parenthesized
664 # with-statements.
665 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
666 with contextlib.ExitStack() as stack: 1g
667 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1g
668 stack.enter_context( 1g
669 pytest_machinery.isolated_config(
670 monkeypatch=monkeypatch,
671 runner=runner,
672 )
673 )
674 result = runner.invoke( 1g
675 cli.derivepassphrase,
676 command_line,
677 input=input,
678 catch_exceptions=False,
679 color=isatty,
680 )
681 assert ( 1g
682 not color
683 or "\x1b[0m" in result.stderr
684 or "\x1b[m" in result.stderr
685 ), "Expected color, but found no ANSI reset sequence"
686 assert color or "\x1b[" not in result.stderr, ( 1g
687 "Expected no color, but found an ANSI control sequence"
688 )
691class TestVersionOutput:
692 """Tests for all command-line interfaces' `--version` output."""
694 def _test(
695 self,
696 command_line: list[str],
697 ) -> VersionOutputData:
698 runner = machinery.CliRunner(mix_stderr=False) 1fbea
699 # TODO(the-13th-letter): Rewrite using parenthesized
700 # with-statements.
701 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
702 with contextlib.ExitStack() as stack: 1fbea
703 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1fbea
704 stack.enter_context( 1fbea
705 pytest_machinery.isolated_config(
706 monkeypatch=monkeypatch,
707 runner=runner,
708 )
709 )
710 result = runner.invoke( 1fbea
711 cli.derivepassphrase,
712 [*command_line, "--version"],
713 catch_exceptions=False,
714 )
715 assert result.clean_exit(empty_stderr=True), "expected clean exit" 1fbea
716 assert result.stdout.strip(), "expected version output" 1fbea
717 return parse_version_output(result.stdout) 1fbea
719 def test_derivepassphrase_version_option_output(
720 self,
721 ) -> None:
722 """The version output states supported features.
724 The version output is parsed using [`parse_version_output`][].
725 Format examples can be found in
726 [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the
727 top-level `derivepassphrase` command, the output should contain
728 the known and supported derivation schemes, and a list of
729 subcommands.
731 As a side effect, [`parse_version_output`][] guarantees that the
732 first line contains both the correct program name as well as the
733 correct program version number.
735 """
736 version_data = self._test([]) 1f
737 actually_known_schemes = dict.fromkeys(_types.DerivationScheme, True) 1f
738 subcommands = set(_types.Subcommand) 1f
739 assert version_data.derivation_schemes == actually_known_schemes 1f
740 assert not version_data.foreign_configuration_formats 1f
741 assert version_data.subcommands == subcommands 1f
742 assert not version_data.features 1f
743 assert not version_data.extras 1f
745 def test_export_version_option_output(
746 self,
747 ) -> None:
748 """The version output states supported features.
750 The version output is parsed using [`parse_version_output`][].
751 Format examples can be found in
752 [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the
753 `export` command, the output should contain the known foreign
754 configuration formats (but not marked as supported), and a list
755 of subcommands.
757 As a side effect, [`parse_version_output`][] guarantees that the
758 first line contains both the correct program name as well as the
759 correct program version number.
761 """
762 version_data = self._test(["export"]) 1e
763 actually_known_formats: dict[str, bool] = { 1e
764 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,
765 _types.ForeignConfigurationFormat.VAULT_V02: False,
766 _types.ForeignConfigurationFormat.VAULT_V03: False,
767 }
768 subcommands = set(_types.ExportSubcommand) 1e
769 assert not version_data.derivation_schemes 1e
770 assert ( 1e
771 version_data.foreign_configuration_formats
772 == actually_known_formats
773 )
774 assert version_data.subcommands == subcommands 1e
775 assert not version_data.features 1e
776 assert not version_data.extras 1e
778 def test_export_vault_version_option_output(
779 self,
780 ) -> None:
781 """The version output states supported features.
783 The version output is parsed using [`parse_version_output`][].
784 Format examples can be found in
785 [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the
786 `export vault` subcommand, the output should contain the
787 vault-specific subset of the known or supported foreign
788 configuration formats, and a list of available PEP 508 extras.
790 As a side effect, [`parse_version_output`][] guarantees that the
791 first line contains both the correct program name as well as the
792 correct program version number.
794 """
795 version_data = self._test(["export", "vault"]) 1b
796 actually_known_formats: dict[str, bool] = {} 1b
797 actually_enabled_extras: set[str] = set() 1b
798 with contextlib.suppress(ModuleNotFoundError): 1b
799 from derivepassphrase.exporter import storeroom, vault_native # noqa: I001,PLC0415 1b
801 actually_known_formats.update({ 1b
802 _types.ForeignConfigurationFormat.VAULT_STOREROOM: not storeroom.STUBBED,
803 _types.ForeignConfigurationFormat.VAULT_V02: not vault_native.STUBBED,
804 _types.ForeignConfigurationFormat.VAULT_V03: not vault_native.STUBBED,
805 })
806 with contextlib.suppress(ModuleNotFoundError): 1b
807 import cryptography # noqa: F401,PLC0415 1b
809 actually_enabled_extras.add(_types.PEP508Extra.EXPORT) 1b
810 assert not version_data.derivation_schemes 1b
811 assert ( 1b
812 version_data.foreign_configuration_formats
813 == actually_known_formats
814 )
815 assert not version_data.subcommands 1b
816 assert not version_data.features 1b
817 assert version_data.extras == actually_enabled_extras 1b
819 def test_vault_version_option_output(
820 self,
821 ) -> None:
822 """The version output states supported features.
824 The version output is parsed using [`parse_version_output`][].
825 Format examples can be found in
826 [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the
827 vault command, the output should not contain anything beyond the
828 first paragraph.
830 As a side effect, [`parse_version_output`][] guarantees that the
831 first line contains both the correct program name as well as the
832 correct program version number.
834 """
835 version_data = self._test(["vault"]) 1a
836 ssh_key_supported = True 1a
838 def react_to_notimplementederror( 1a
839 _exc: BaseException, 1a
840 ) -> None: # pragma: no cover[unused] 1a
841 nonlocal ssh_key_supported
842 ssh_key_supported = False
844 with exceptiongroup.catch({ # noqa: SIM117 1a
845 NotImplementedError: react_to_notimplementederror,
846 Exception: lambda *_args: None,
847 }):
848 with ssh_agent.SSHAgentClient.ensure_agent_subcontext(): 1a
849 pass 1a
850 features: dict[str, bool] = { 1a
851 _types.Feature.SSH_KEY: ssh_key_supported,
852 }
853 assert not version_data.derivation_schemes 1a
854 assert not version_data.foreign_configuration_formats 1a
855 assert not version_data.subcommands 1a
856 assert version_data.features == features 1a
857 assert not version_data.extras 1a