Marco Ricci commited on 2025-02-06 14:27:21
Zeige 4 geänderte Dateien mit 278 Einfügungen und 56 Löschungen.
Reintroduce the previous git-like editor interface as the "modern" editor interface to the "vault" subcommand, and provide explicit options `--modern-editor-interface` and `--vault-legacy-editor-interface`, defaulting to the latter for compatibility. Furthermore, for the legacy interface, introduce a backup copy of the notes to guard against accidental data loss, because the legacy interface offers no way to abort mid-editing, making fatal mistakes all the more likely.
... | ... |
@@ -168,6 +168,7 @@ config_filename_table = { |
168 | 168 |
# TODO(the-13th-letter): Remove the old settings.json file. |
169 | 169 |
# https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file |
170 | 170 |
'old settings.json': 'settings.json', |
171 |
+ 'notes backup': 'old-notes.txt', |
|
171 | 172 |
} |
172 | 173 |
|
173 | 174 |
|
... | ... |
@@ -809,7 +809,8 @@ class Label(enum.Enum): |
809 | 809 |
"""""" |
810 | 810 |
DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented( |
811 | 811 |
"This instruction text is shown above the user's old stored notes " |
812 |
- 'for this service, if any. ' |
|
812 |
+ 'for this service, if any, if the recommended ' |
|
813 |
+ '"modern" editor interface is used. ' |
|
813 | 814 |
'The next line is the cut marking defined in ' |
814 | 815 |
'Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER.' |
815 | 816 |
)( |
... | ... |
@@ -826,6 +827,21 @@ class Label(enum.Enum): |
826 | 827 |
""", |
827 | 828 |
) |
828 | 829 |
"""""" |
830 |
+ DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented( |
|
831 |
+ "This instruction text is shown above the user's old stored notes " |
|
832 |
+ 'for this service, if any, if the vault(1)-compatible ' |
|
833 |
+ '"legacy" editor interface is used. ' |
|
834 |
+ 'The interface does not support commentary in the notes, ' |
|
835 |
+ 'so we fill this with obvious placeholder text instead. ' |
|
836 |
+ '(Please replace this with what *your* language/culture would ' |
|
837 |
+ 'obviously recognize as placeholder text.)' |
|
838 |
+ )( |
|
839 |
+ 'Label :: Help text :: Explanation', |
|
840 |
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' |
|
841 |
+ 'sed do eiusmod tempor incididunt ut labore ' |
|
842 |
+ 'et dolore magna aliqua.', |
|
843 |
+ ) |
|
844 |
+ """""" |
|
829 | 845 |
DEPRECATED_COMMAND_LABEL = commented( |
830 | 846 |
'We use this format string to indicate, at the beginning ' |
831 | 847 |
"of a command's help text, that this command is deprecated.", |
... | ... |
@@ -1082,6 +1098,21 @@ class Label(enum.Enum): |
1082 | 1098 |
'when exporting, export as JSON (default) or POSIX sh', |
1083 | 1099 |
) |
1084 | 1100 |
"""""" |
1101 |
+ DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented( |
|
1102 |
+ 'The corresponding option is displayed as ' |
|
1103 |
+ '"--modern-editor-interface / --vault-legacy-editor-interface", ' |
|
1104 |
+ 'so you may want to hint that the default (legacy) ' |
|
1105 |
+ 'is the second of those options. ' |
|
1106 |
+ 'Though the vault(1) legacy editor interface clearly has deficiencies ' |
|
1107 |
+ 'and (in my opinion) should only be used for compatibility purposes, ' |
|
1108 |
+ 'the one-line help text should try not to sound too judgmental, ' |
|
1109 |
+ 'if possible.', |
|
1110 |
+ )( |
|
1111 |
+ 'Label :: Help text :: One-line description', |
|
1112 |
+ 'edit notes using the modern editor interface ' |
|
1113 |
+ 'or the vault-like legacy one (default)', |
|
1114 |
+ ) |
|
1115 |
+ """""" |
|
1085 | 1116 |
|
1086 | 1117 |
EXPORT_VAULT_FORMAT_METAVAR_FMT = commented( |
1087 | 1118 |
'', |
... | ... |
@@ -1785,6 +1816,16 @@ class WarnMsgTemplate(enum.Enum): |
1785 | 1816 |
'because a key is also set.', |
1786 | 1817 |
) |
1787 | 1818 |
"""""" |
1819 |
+ LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented( |
|
1820 |
+ '', |
|
1821 |
+ )( |
|
1822 |
+ 'Warning message', |
|
1823 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
1824 |
+ 'which does not allow aborting mid-edit. ' |
|
1825 |
+ 'A backup copy of the old notes was saved to {filename!r} ' |
|
1826 |
+ 'to guard against editing mistakes.', |
|
1827 |
+ flags='python-brace-format', |
|
1828 |
+ ) |
|
1788 | 1829 |
PASSPHRASE_NOT_NORMALIZED = commented( |
1789 | 1830 |
'The key is a (vault) configuration key, in JSONPath syntax, ' |
1790 | 1831 |
'typically "$.global" for the global passphrase or ' |
... | ... |
@@ -593,6 +593,15 @@ def derivepassphrase_export_vault( |
593 | 593 |
), |
594 | 594 |
cls=cli_machinery.CompatibilityOption, |
595 | 595 |
) |
596 |
+@click.option( |
|
597 |
+ '--modern-editor-interface/--vault-legacy-editor-interface', |
|
598 |
+ 'modern_editor_interface', |
|
599 |
+ default=False, |
|
600 |
+ help=_msg.TranslatedString( |
|
601 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT |
|
602 |
+ ), |
|
603 |
+ cls=cli_machinery.CompatibilityOption, |
|
604 |
+) |
|
596 | 605 |
@cli_machinery.version_option |
597 | 606 |
@cli_machinery.color_forcing_pseudo_option |
598 | 607 |
@cli_machinery.standard_logging_options |
... | ... |
@@ -629,6 +638,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
629 | 638 |
overwrite_config: bool = False, |
630 | 639 |
unset_settings: Sequence[str] = (), |
631 | 640 |
export_as: Literal['json', 'sh'] = 'json', |
641 |
+ modern_editor_interface: bool = False, |
|
632 | 642 |
) -> None: |
633 | 643 |
"""Derive a passphrase using the vault(1) derivation scheme. |
634 | 644 |
|
... | ... |
@@ -722,6 +732,12 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
722 | 732 |
Command-line argument `--export-as`. If given together with |
723 | 733 |
`--export`, selects the format to export the current |
724 | 734 |
configuration as: JSON ("json", default) or POSIX sh ("sh"). |
735 |
+ modern_editor_interface: |
|
736 |
+ Command-line arguments `--modern-editor-interface` (True) |
|
737 |
+ and `--vault-legacy-editor-interface` (False). Controls |
|
738 |
+ whether editing notes uses a modern editor interface |
|
739 |
+ (supporting comments and aborting) or a vault(1)-compatible |
|
740 |
+ legacy editor interface (WYSIWYG notes contents). |
|
725 | 741 |
|
726 | 742 |
""" # noqa: DOC501 |
727 | 743 |
logger = logging.getLogger(PROG_NAME) |
... | ... |
@@ -996,7 +1012,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
996 | 1012 |
extra={'color': ctx.color}, |
997 | 1013 |
) |
998 | 1014 |
|
999 |
- if delete_service_settings: |
|
1015 |
+ if delete_service_settings: # noqa: PLR1702 |
|
1000 | 1016 |
assert service is not None |
1001 | 1017 |
configuration = get_config() |
1002 | 1018 |
if service in configuration['services']: |
... | ... |
@@ -1396,15 +1412,55 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1396 | 1412 |
notes_marker = _msg.TranslatedString( |
1397 | 1413 |
_msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
1398 | 1414 |
) |
1415 |
+ notes_legacy_instructions = _msg.TranslatedString( |
|
1416 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT |
|
1417 |
+ ) |
|
1399 | 1418 |
old_notes_value = subtree.get('notes', '') |
1419 |
+ if modern_editor_interface: |
|
1400 | 1420 |
text = '\n'.join([ |
1401 | 1421 |
str(notes_instructions), |
1402 | 1422 |
str(notes_marker), |
1403 | 1423 |
old_notes_value, |
1404 | 1424 |
]) |
1425 |
+ else: |
|
1426 |
+ text = str(notes_legacy_instructions) |
|
1405 | 1427 |
notes_value = click.edit(text=text, require_save=False) |
1406 | 1428 |
assert notes_value is not None |
1407 |
- if notes_value.strip() != old_notes_value.strip(): |
|
1429 |
+ if ( |
|
1430 |
+ not modern_editor_interface |
|
1431 |
+ and notes_value.strip() != old_notes_value.strip() |
|
1432 |
+ ): |
|
1433 |
+ backup_file = cli_helpers.config_filename( |
|
1434 |
+ subsystem='notes backup' |
|
1435 |
+ ) |
|
1436 |
+ backup_file.write_text(old_notes_value, encoding='UTF-8') |
|
1437 |
+ logger.warning( |
|
1438 |
+ _msg.TranslatedString( |
|
1439 |
+ _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP, |
|
1440 |
+ filename=str(backup_file), |
|
1441 |
+ ), |
|
1442 |
+ extra={'color': ctx.color}, |
|
1443 |
+ ) |
|
1444 |
+ subtree['notes'] = notes_value.strip() |
|
1445 |
+ elif ( |
|
1446 |
+ modern_editor_interface |
|
1447 |
+ and notes_value.strip() != text.strip() |
|
1448 |
+ ): |
|
1449 |
+ notes_lines = collections.deque( |
|
1450 |
+ notes_value.splitlines(True) # noqa: FBT003 |
|
1451 |
+ ) |
|
1452 |
+ while notes_lines: |
|
1453 |
+ line = notes_lines.popleft() |
|
1454 |
+ if line.startswith(str(notes_marker)): |
|
1455 |
+ notes_value = ''.join(notes_lines) |
|
1456 |
+ break |
|
1457 |
+ else: |
|
1458 |
+ if not notes_value.strip(): |
|
1459 |
+ err( |
|
1460 |
+ _msg.TranslatedString( |
|
1461 |
+ _msg.ErrMsgTemplate.USER_ABORTED_EDIT |
|
1462 |
+ ) |
|
1463 |
+ ) |
|
1408 | 1464 |
subtree['notes'] = notes_value.strip() |
1409 | 1465 |
put_config(configuration) |
1410 | 1466 |
else: |
... | ... |
@@ -639,6 +639,8 @@ class Parametrize(types.SimpleNamespace): |
639 | 639 |
'--merge-existing', |
640 | 640 |
'--unset', |
641 | 641 |
'--export-as', |
642 |
+ '--modern-editor-interface', |
|
643 |
+ '--vault-legacy-editor-interface', |
|
642 | 644 |
}), |
643 | 645 |
id='derivepassphrase-vault', |
644 | 646 |
), |
... | ... |
@@ -2468,6 +2470,15 @@ class TestCLI: |
2468 | 2470 |
'expected error exit and known error message' |
2469 | 2471 |
) |
2470 | 2472 |
|
2473 |
+ @pytest.mark.parametrize( |
|
2474 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
2475 |
+ ) |
|
2476 |
+ @hypothesis.settings( |
|
2477 |
+ suppress_health_check=[ |
|
2478 |
+ *hypothesis.settings().suppress_health_check, |
|
2479 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
2480 |
+ ], |
|
2481 |
+ ) |
|
2471 | 2482 |
@hypothesis.given( |
2472 | 2483 |
notes=strategies.text( |
2473 | 2484 |
strategies.characters( |
... | ... |
@@ -2479,6 +2490,8 @@ class TestCLI: |
2479 | 2490 |
) |
2480 | 2491 |
def test_220_edit_notes_successfully( |
2481 | 2492 |
self, |
2493 |
+ caplog: pytest.LogCaptureFixture, |
|
2494 |
+ modern_editor_interface: bool, |
|
2482 | 2495 |
notes: str, |
2483 | 2496 |
) -> None: |
2484 | 2497 |
"""Editing notes works.""" |
... | ... |
@@ -2487,6 +2500,8 @@ class TestCLI: |
2487 | 2500 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
2488 | 2501 |
{notes} |
2489 | 2502 |
""" |
2503 |
+ # Reset caplog between hypothesis runs. |
|
2504 |
+ caplog.clear() |
|
2490 | 2505 |
runner = click.testing.CliRunner(mix_stderr=False) |
2491 | 2506 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2492 | 2507 |
# with-statements. |
... | ... |
@@ -2503,46 +2518,62 @@ class TestCLI: |
2503 | 2518 |
}, |
2504 | 2519 |
) |
2505 | 2520 |
) |
2521 |
+ notes_backup_file = cli_helpers.config_filename( |
|
2522 |
+ subsystem='notes backup' |
|
2523 |
+ ) |
|
2524 |
+ notes_backup_file.write_text( |
|
2525 |
+ 'These backup notes are left over from the previous session.', |
|
2526 |
+ encoding='UTF-8', |
|
2527 |
+ ) |
|
2506 | 2528 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: edit_result) |
2507 | 2529 |
result_ = runner.invoke( |
2508 | 2530 |
cli.derivepassphrase_vault, |
2509 |
- ['--config', '--notes', '--', 'sv'], |
|
2531 |
+ [ |
|
2532 |
+ '--config', |
|
2533 |
+ '--notes', |
|
2534 |
+ '--modern-editor-interface' |
|
2535 |
+ if modern_editor_interface |
|
2536 |
+ else '--vault-legacy-editor-interface', |
|
2537 |
+ '--', |
|
2538 |
+ 'sv', |
|
2539 |
+ ], |
|
2510 | 2540 |
catch_exceptions=False, |
2511 | 2541 |
) |
2512 | 2542 |
result = tests.ReadableResult.parse(result_) |
2513 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
2543 |
+ assert result.clean_exit(), 'expected clean exit' |
|
2544 |
+ assert all(map(is_warning_line, result.stderr.splitlines(True))) |
|
2545 |
+ assert modern_editor_interface or tests.warning_emitted( |
|
2546 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
2547 |
+ 'which does not allow aborting mid-edit. ' |
|
2548 |
+ 'A backup copy of the old notes was saved', |
|
2549 |
+ caplog.record_tuples, |
|
2550 |
+ ), 'expected known warning message in stderr' |
|
2551 |
+ assert ( |
|
2552 |
+ modern_editor_interface |
|
2553 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
2554 |
+ == 'Contents go here' |
|
2555 |
+ ) |
|
2514 | 2556 |
with cli_helpers.config_filename(subsystem='vault').open( |
2515 | 2557 |
encoding='UTF-8' |
2516 | 2558 |
) as infile: |
2517 | 2559 |
config = json.load(infile) |
2518 | 2560 |
assert config == { |
2519 | 2561 |
'global': {'phrase': 'abc'}, |
2520 |
- 'services': {'sv': {'notes': edit_result.strip()}}, |
|
2562 |
+ 'services': { |
|
2563 |
+ 'sv': { |
|
2564 |
+ 'notes': notes.strip() |
|
2565 |
+ if modern_editor_interface |
|
2566 |
+ else edit_result.strip() |
|
2567 |
+ } |
|
2568 |
+ }, |
|
2521 | 2569 |
} |
2522 | 2570 |
|
2523 | 2571 |
@pytest.mark.parametrize( |
2524 |
- 'edit_func_name', |
|
2572 |
+ ['edit_func_name', 'modern_editor_interface'], |
|
2525 | 2573 |
[ |
2526 |
- pytest.param( |
|
2527 |
- 'empty', |
|
2528 |
- marks=[ |
|
2529 |
- pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
2530 |
- ], |
|
2531 |
- ), |
|
2532 |
- 'space', |
|
2533 |
- ], |
|
2534 |
- ) |
|
2535 |
- # Skip the "target", "shrink" and "explain" phases on this test because |
|
2536 |
- # one of its parametrizations is marked xfail, and we don't want to do |
|
2537 |
- # a lot of extra work that will be thrown away anyway. |
|
2538 |
- # |
|
2539 |
- # TODO(the-13th-letter): remove this settings decorator once the |
|
2540 |
- # xfail'ing parametrization is fixed. |
|
2541 |
- @hypothesis.settings( |
|
2542 |
- phases=[ |
|
2543 |
- hypothesis.Phase.explicit, |
|
2544 |
- hypothesis.Phase.reuse, |
|
2545 |
- hypothesis.Phase.generate, |
|
2574 |
+ pytest.param('empty', True, id='empty'), |
|
2575 |
+ pytest.param('space', False, id='space-legacy'), |
|
2576 |
+ pytest.param('space', True, id='space-modern'), |
|
2546 | 2577 |
], |
2547 | 2578 |
) |
2548 | 2579 |
@hypothesis.given( |
... | ... |
@@ -2557,16 +2588,18 @@ class TestCLI: |
2557 | 2588 |
def test_221_edit_notes_noop( |
2558 | 2589 |
self, |
2559 | 2590 |
edit_func_name: Literal['empty', 'space'], |
2591 |
+ modern_editor_interface: bool, |
|
2560 | 2592 |
notes: str, |
2561 | 2593 |
) -> None: |
2562 | 2594 |
"""Abandoning edited notes works.""" |
2563 | 2595 |
|
2564 |
- def empty(text: str, *_args: Any, **_kwargs: Any) -> None: |
|
2596 |
+ def empty(text: str, *_args: Any, **_kwargs: Any) -> str: |
|
2565 | 2597 |
del text |
2598 |
+ return '' |
|
2566 | 2599 |
|
2567 | 2600 |
def space(text: str, *_args: Any, **_kwargs: Any) -> str: |
2568 | 2601 |
del text |
2569 |
- return ' ' + notes + '\n\n\n\n\n\n' |
|
2602 |
+ return ' ' + notes.strip() + '\n\n\n\n\n\n' |
|
2570 | 2603 |
|
2571 | 2604 |
edit_funcs = {'empty': empty, 'space': space} |
2572 | 2605 |
runner = click.testing.CliRunner(mix_stderr=False) |
... | ... |
@@ -2581,30 +2614,60 @@ class TestCLI: |
2581 | 2614 |
runner=runner, |
2582 | 2615 |
vault_config={ |
2583 | 2616 |
'global': {'phrase': 'abc'}, |
2584 |
- 'services': {'sv': {'notes': notes}}, |
|
2617 |
+ 'services': {'sv': {'notes': notes.strip()}}, |
|
2585 | 2618 |
}, |
2586 | 2619 |
) |
2587 | 2620 |
) |
2588 |
- |
|
2621 |
+ notes_backup_file = cli_helpers.config_filename( |
|
2622 |
+ subsystem='notes backup' |
|
2623 |
+ ) |
|
2624 |
+ notes_backup_file.write_text( |
|
2625 |
+ 'These backup notes are left over from the previous session.', |
|
2626 |
+ encoding='UTF-8', |
|
2627 |
+ ) |
|
2589 | 2628 |
monkeypatch.setattr(click, 'edit', edit_funcs[edit_func_name]) |
2590 | 2629 |
result_ = runner.invoke( |
2591 | 2630 |
cli.derivepassphrase_vault, |
2592 |
- ['--config', '--notes', '--', 'sv'], |
|
2631 |
+ [ |
|
2632 |
+ '--config', |
|
2633 |
+ '--notes', |
|
2634 |
+ '--modern-editor-interface' |
|
2635 |
+ if modern_editor_interface |
|
2636 |
+ else '--vault-legacy-editor-interface', |
|
2637 |
+ '--', |
|
2638 |
+ 'sv', |
|
2639 |
+ ], |
|
2593 | 2640 |
catch_exceptions=False, |
2594 | 2641 |
) |
2595 | 2642 |
result = tests.ReadableResult.parse(result_) |
2596 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
2643 |
+ assert result.clean_exit(empty_stderr=True) or result.error_exit( |
|
2644 |
+ error='the user aborted the request' |
|
2645 |
+ ), 'expected clean exit' |
|
2646 |
+ assert ( |
|
2647 |
+ modern_editor_interface |
|
2648 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
2649 |
+ == 'These backup notes are left over from the previous session.' |
|
2650 |
+ ) |
|
2597 | 2651 |
with cli_helpers.config_filename(subsystem='vault').open( |
2598 | 2652 |
encoding='UTF-8' |
2599 | 2653 |
) as infile: |
2600 | 2654 |
config = json.load(infile) |
2601 | 2655 |
assert config == { |
2602 | 2656 |
'global': {'phrase': 'abc'}, |
2603 |
- 'services': {'sv': {'notes': notes}}, |
|
2657 |
+ 'services': {'sv': {'notes': notes.strip()}}, |
|
2604 | 2658 |
} |
2605 | 2659 |
|
2606 | 2660 |
# TODO(the-13th-letter): Keep this behavior or not, with or without |
2607 | 2661 |
# warning? |
2662 |
+ @pytest.mark.parametrize( |
|
2663 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
2664 |
+ ) |
|
2665 |
+ @hypothesis.settings( |
|
2666 |
+ suppress_health_check=[ |
|
2667 |
+ *hypothesis.settings().suppress_health_check, |
|
2668 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
2669 |
+ ], |
|
2670 |
+ ) |
|
2608 | 2671 |
@hypothesis.given( |
2609 | 2672 |
notes=strategies.text( |
2610 | 2673 |
strategies.characters( |
... | ... |
@@ -2616,6 +2679,8 @@ class TestCLI: |
2616 | 2679 |
) |
2617 | 2680 |
def test_222_edit_notes_marker_removed( |
2618 | 2681 |
self, |
2682 |
+ caplog: pytest.LogCaptureFixture, |
|
2683 |
+ modern_editor_interface: bool, |
|
2619 | 2684 |
notes: str, |
2620 | 2685 |
) -> None: |
2621 | 2686 |
"""Removing the notes marker still saves the notes. |
... | ... |
@@ -2627,6 +2692,8 @@ class TestCLI: |
2627 | 2692 |
cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
2628 | 2693 |
) |
2629 | 2694 |
hypothesis.assume(str(notes_marker) not in notes.strip()) |
2695 |
+ # Reset caplog between hypothesis runs. |
|
2696 |
+ caplog.clear() |
|
2630 | 2697 |
runner = click.testing.CliRunner(mix_stderr=False) |
2631 | 2698 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2632 | 2699 |
# with-statements. |
... | ... |
@@ -2643,14 +2710,43 @@ class TestCLI: |
2643 | 2710 |
}, |
2644 | 2711 |
) |
2645 | 2712 |
) |
2713 |
+ notes_backup_file = cli_helpers.config_filename( |
|
2714 |
+ subsystem='notes backup' |
|
2715 |
+ ) |
|
2716 |
+ notes_backup_file.write_text( |
|
2717 |
+ 'These backup notes are left over from the previous session.', |
|
2718 |
+ encoding='UTF-8', |
|
2719 |
+ ) |
|
2646 | 2720 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: notes) |
2647 | 2721 |
result_ = runner.invoke( |
2648 | 2722 |
cli.derivepassphrase_vault, |
2649 |
- ['--config', '--notes', '--', 'sv'], |
|
2723 |
+ [ |
|
2724 |
+ '--config', |
|
2725 |
+ '--notes', |
|
2726 |
+ '--modern-editor-interface' |
|
2727 |
+ if modern_editor_interface |
|
2728 |
+ else '--vault-legacy-editor-interface', |
|
2729 |
+ '--', |
|
2730 |
+ 'sv', |
|
2731 |
+ ], |
|
2650 | 2732 |
catch_exceptions=False, |
2651 | 2733 |
) |
2652 | 2734 |
result = tests.ReadableResult.parse(result_) |
2653 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
2735 |
+ assert result.clean_exit(), 'expected clean exit' |
|
2736 |
+ assert not result.stderr or all( |
|
2737 |
+ map(is_warning_line, result.stderr.splitlines(True)) |
|
2738 |
+ ) |
|
2739 |
+ assert not caplog.record_tuples or tests.warning_emitted( |
|
2740 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
2741 |
+ 'which does not allow aborting mid-edit. ' |
|
2742 |
+ 'A backup copy of the old notes was saved', |
|
2743 |
+ caplog.record_tuples, |
|
2744 |
+ ), 'expected known warning message in stderr' |
|
2745 |
+ assert ( |
|
2746 |
+ modern_editor_interface |
|
2747 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
2748 |
+ == 'Contents go here' |
|
2749 |
+ ) |
|
2654 | 2750 |
with cli_helpers.config_filename(subsystem='vault').open( |
2655 | 2751 |
encoding='UTF-8' |
2656 | 2752 |
) as infile: |
... | ... |
@@ -2660,20 +2756,6 @@ class TestCLI: |
2660 | 2756 |
'services': {'sv': {'notes': notes.strip()}}, |
2661 | 2757 |
} |
2662 | 2758 |
|
2663 |
- @pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
2664 |
- # Skip the "target", "shrink" and "explain" phases on this test because |
|
2665 |
- # this test is marked xfail, and we don't want to do a lot of extra work |
|
2666 |
- # that will be thrown away anyway. |
|
2667 |
- # |
|
2668 |
- # TODO(the-13th-letter): remove this settings decorator once the xfail |
|
2669 |
- # is fixed. |
|
2670 |
- @hypothesis.settings( |
|
2671 |
- phases=[ |
|
2672 |
- hypothesis.Phase.explicit, |
|
2673 |
- hypothesis.Phase.reuse, |
|
2674 |
- hypothesis.Phase.generate, |
|
2675 |
- ], |
|
2676 |
- ) |
|
2677 | 2759 |
@hypothesis.given( |
2678 | 2760 |
notes=strategies.text( |
2679 | 2761 |
strategies.characters( |
... | ... |
@@ -2687,7 +2769,11 @@ class TestCLI: |
2687 | 2769 |
self, |
2688 | 2770 |
notes: str, |
2689 | 2771 |
) -> None: |
2690 |
- """Aborting editing notes works.""" |
|
2772 |
+ """Aborting editing notes works. |
|
2773 |
+ |
|
2774 |
+ Aborting is only supported with the modern editor interface. |
|
2775 |
+ |
|
2776 |
+ """ |
|
2691 | 2777 |
runner = click.testing.CliRunner(mix_stderr=False) |
2692 | 2778 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2693 | 2779 |
# with-statements. |
... | ... |
@@ -2707,7 +2793,13 @@ class TestCLI: |
2707 | 2793 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '') |
2708 | 2794 |
result_ = runner.invoke( |
2709 | 2795 |
cli.derivepassphrase_vault, |
2710 |
- ['--config', '--notes', '--', 'sv'], |
|
2796 |
+ [ |
|
2797 |
+ '--config', |
|
2798 |
+ '--notes', |
|
2799 |
+ '--modern-editor-interface', |
|
2800 |
+ '--', |
|
2801 |
+ 'sv', |
|
2802 |
+ ], |
|
2711 | 2803 |
catch_exceptions=False, |
2712 | 2804 |
) |
2713 | 2805 |
result = tests.ReadableResult.parse(result_) |
... | ... |
@@ -2723,11 +2815,14 @@ class TestCLI: |
2723 | 2815 |
'services': {'sv': {'notes': notes.strip()}}, |
2724 | 2816 |
} |
2725 | 2817 |
|
2726 |
- @pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
2727 | 2818 |
def test_223a_edit_empty_notes_abort( |
2728 | 2819 |
self, |
2729 | 2820 |
) -> None: |
2730 |
- """Aborting editing notes works even if no notes are stored yet.""" |
|
2821 |
+ """Aborting editing notes works even if no notes are stored yet. |
|
2822 |
+ |
|
2823 |
+ Aborting is only supported with the modern editor interface. |
|
2824 |
+ |
|
2825 |
+ """ |
|
2731 | 2826 |
runner = click.testing.CliRunner(mix_stderr=False) |
2732 | 2827 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2733 | 2828 |
# with-statements. |
... | ... |
@@ -2747,7 +2842,13 @@ class TestCLI: |
2747 | 2842 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '') |
2748 | 2843 |
result_ = runner.invoke( |
2749 | 2844 |
cli.derivepassphrase_vault, |
2750 |
- ['--config', '--notes', '--', 'sv'], |
|
2845 |
+ [ |
|
2846 |
+ '--config', |
|
2847 |
+ '--notes', |
|
2848 |
+ '--modern-editor-interface', |
|
2849 |
+ '--', |
|
2850 |
+ 'sv', |
|
2851 |
+ ], |
|
2751 | 2852 |
catch_exceptions=False, |
2752 | 2853 |
) |
2753 | 2854 |
result = tests.ReadableResult.parse(result_) |
... | ... |
@@ -2763,6 +2864,9 @@ class TestCLI: |
2763 | 2864 |
'services': {}, |
2764 | 2865 |
} |
2765 | 2866 |
|
2867 |
+ @pytest.mark.parametrize( |
|
2868 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
2869 |
+ ) |
|
2766 | 2870 |
@hypothesis.settings( |
2767 | 2871 |
suppress_health_check=[ |
2768 | 2872 |
*hypothesis.settings().suppress_health_check, |
... | ... |
@@ -2780,6 +2884,7 @@ class TestCLI: |
2780 | 2884 |
def test_223b_edit_notes_fail_config_option_missing( |
2781 | 2885 |
self, |
2782 | 2886 |
caplog: pytest.LogCaptureFixture, |
2887 |
+ modern_editor_interface: bool, |
|
2783 | 2888 |
notes: str, |
2784 | 2889 |
) -> None: |
2785 | 2890 |
"""Editing notes fails (and warns) if `--config` is missing.""" |
... | ... |
@@ -2810,10 +2915,24 @@ class TestCLI: |
2810 | 2915 |
def raiser(*_args: Any, **_kwargs: Any) -> NoReturn: |
2811 | 2916 |
pytest.fail(EDIT_ATTEMPTED) |
2812 | 2917 |
|
2918 |
+ notes_backup_file = cli_helpers.config_filename( |
|
2919 |
+ subsystem='notes backup' |
|
2920 |
+ ) |
|
2921 |
+ notes_backup_file.write_text( |
|
2922 |
+ 'These backup notes are left over from the previous session.', |
|
2923 |
+ encoding='UTF-8', |
|
2924 |
+ ) |
|
2813 | 2925 |
monkeypatch.setattr(click, 'edit', raiser) |
2814 | 2926 |
result_ = runner.invoke( |
2815 | 2927 |
cli.derivepassphrase_vault, |
2816 |
- ['--notes', '--', DUMMY_SERVICE], |
|
2928 |
+ [ |
|
2929 |
+ '--notes', |
|
2930 |
+ '--modern-editor-interface' |
|
2931 |
+ if modern_editor_interface |
|
2932 |
+ else '--vault-legacy-editor-interface', |
|
2933 |
+ '--', |
|
2934 |
+ DUMMY_SERVICE, |
|
2935 |
+ ], |
|
2817 | 2936 |
catch_exceptions=False, |
2818 | 2937 |
) |
2819 | 2938 |
result = tests.ReadableResult.parse(result_) |
... | ... |
@@ -2832,6 +2951,11 @@ class TestCLI: |
2832 | 2951 |
'No notes will be edited.', |
2833 | 2952 |
caplog.record_tuples, |
2834 | 2953 |
), 'expected known warning message in stderr' |
2954 |
+ assert ( |
|
2955 |
+ modern_editor_interface |
|
2956 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
2957 |
+ == 'These backup notes are left over from the previous session.' |
|
2958 |
+ ) |
|
2835 | 2959 |
with cli_helpers.config_filename(subsystem='vault').open( |
2836 | 2960 |
encoding='UTF-8' |
2837 | 2961 |
) as infile: |
2838 | 2962 |