Marco Ricci commited on 2024-12-07 09:30:38
Zeige 4 geänderte Dateien mit 281 Einfügungen und 69 Löschungen.
Given the previous changes to warning and error reporting, the test suite needs new helper code to deal with them. Again, the changes to mosts tests are straight-forward (the location and formatting of messages changes slightly), but the new logging machinery itself now also needs to be tested... particularly the code that hooks into Python's logging system, into Python's warning system, and between those two. `pytest` actually includes test fixtures to capture and interact with warnings and logging calls, but these work on the library level: warnings are captured as warnings, logging calls are captured as log records. To be useful to application testing, it is necessary to inspect and assert both the low-level warnings/log records as well as the standard error output they ultimately translate to. The `hypothesis`-based tests need particular (manual) care, because `hypothesis` rejects tests using function-scoped `pytest` fixtures.
... | ... |
@@ -10,7 +10,9 @@ import copy |
10 | 10 |
import enum |
11 | 11 |
import importlib.util |
12 | 12 |
import json |
13 |
+import logging |
|
13 | 14 |
import os |
15 |
+import re |
|
14 | 16 |
import shlex |
15 | 17 |
import stat |
16 | 18 |
import sys |
... | ... |
@@ -28,7 +30,7 @@ from derivepassphrase import _types, cli, ssh_agent, vault |
28 | 30 |
__all__ = () |
29 | 31 |
|
30 | 32 |
if TYPE_CHECKING: |
31 |
- from collections.abc import Iterator, Mapping |
|
33 |
+ from collections.abc import Callable, Iterator, Mapping, Sequence |
|
32 | 34 |
|
33 | 35 |
import click.testing |
34 | 36 |
from typing_extensions import Any, NotRequired, TypedDict |
... | ... |
@@ -1344,6 +1346,14 @@ hypothesis_settings_coverage_compatible = ( |
1344 | 1346 |
else hypothesis.settings() |
1345 | 1347 |
) |
1346 | 1348 |
|
1349 |
+hypothesis_settings_coverage_compatible_with_caplog = hypothesis.settings( |
|
1350 |
+ parent=hypothesis_settings_coverage_compatible, |
|
1351 |
+ suppress_health_check={ |
|
1352 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
1353 |
+ } |
|
1354 |
+ | set(hypothesis_settings_coverage_compatible.suppress_health_check), |
|
1355 |
+) |
|
1356 |
+ |
|
1347 | 1357 |
|
1348 | 1358 |
def list_keys(self: Any = None) -> list[_types.KeyCommentPair]: |
1349 | 1359 |
del self # Unused. |
... | ... |
@@ -1403,7 +1413,11 @@ def isolated_config( |
1403 | 1413 |
) -> Iterator[None]: |
1404 | 1414 |
prog_name = cli.PROG_NAME |
1405 | 1415 |
env_name = prog_name.replace(' ', '_').upper() + '_PATH' |
1406 |
- with runner.isolated_filesystem(): |
|
1416 |
+ with ( |
|
1417 |
+ runner.isolated_filesystem(), |
|
1418 |
+ cli.StandardCLILogging.ensure_standard_logging(), |
|
1419 |
+ cli.StandardCLILogging.ensure_standard_warnings_logging(), |
|
1420 |
+ ): |
|
1407 | 1421 |
monkeypatch.setenv('HOME', os.getcwd()) |
1408 | 1422 |
monkeypatch.setenv('USERPROFILE', os.getcwd()) |
1409 | 1423 |
monkeypatch.delenv(env_name, raising=False) |
... | ... |
@@ -1575,7 +1589,10 @@ class ReadableResult(NamedTuple): |
1575 | 1589 |
) |
1576 | 1590 |
|
1577 | 1591 |
def error_exit( |
1578 |
- self, *, error: str | type[BaseException] = BaseException |
|
1592 |
+ self, |
|
1593 |
+ *, |
|
1594 |
+ error: str | re.Pattern[str] | type[BaseException] = BaseException, |
|
1595 |
+ record_tuples: Sequence[tuple[str, int, str]] = (), |
|
1579 | 1596 |
) -> bool: |
1580 | 1597 |
"""Return whether the invocation exited uncleanly. |
1581 | 1598 |
|
... | ... |
@@ -1585,15 +1602,31 @@ class ReadableResult(NamedTuple): |
1585 | 1602 |
code, or an expected exception type. |
1586 | 1603 |
|
1587 | 1604 |
""" |
1605 |
+ |
|
1606 |
+ def error_match(error: str | re.Pattern[str], line: str) -> bool: |
|
1607 |
+ return ( |
|
1608 |
+ error in line |
|
1609 |
+ if isinstance(error, str) |
|
1610 |
+ else error.match(line) is not None |
|
1611 |
+ ) |
|
1612 |
+ |
|
1588 | 1613 |
# Use match/case here once Python 3.9 becomes unsupported. |
1589 |
- if isinstance(error, str): |
|
1614 |
+ if isinstance(error, type): |
|
1615 |
+ return isinstance(self.exception, error) |
|
1616 |
+ else: # noqa: RET505 |
|
1617 |
+ assert isinstance(error, (str, re.Pattern)) |
|
1590 | 1618 |
return ( |
1591 | 1619 |
isinstance(self.exception, SystemExit) |
1592 | 1620 |
and self.exit_code > 0 |
1593 |
- and (not error or error in self.stderr) |
|
1621 |
+ and ( |
|
1622 |
+ not error |
|
1623 |
+ or any( |
|
1624 |
+ error_match(error, line) |
|
1625 |
+ for line in self.stderr.splitlines(True) |
|
1626 |
+ ) |
|
1627 |
+ or error_emitted(error, record_tuples) |
|
1628 |
+ ) |
|
1594 | 1629 |
) |
1595 |
- else: # noqa: RET505 |
|
1596 |
- return isinstance(self.exception, error) |
|
1597 | 1630 |
|
1598 | 1631 |
|
1599 | 1632 |
def parse_sh_export_line(line: str, *, env_name: str) -> str: |
... | ... |
@@ -1617,3 +1650,49 @@ def parse_sh_export_line(line: str, *, env_name: str) -> str: |
1617 | 1650 |
msg = f'Cannot parse sh line: {orig_tokens!r} -> {tokens!r}' |
1618 | 1651 |
raise ValueError(msg) |
1619 | 1652 |
return tokens[1].split('=', 1)[1] |
1653 |
+ |
|
1654 |
+ |
|
1655 |
+def message_emitted_factory( |
|
1656 |
+ level: int, |
|
1657 |
+ *, |
|
1658 |
+ logger_name: str = cli.PROG_NAME, |
|
1659 |
+) -> Callable[[str | re.Pattern[str], Sequence[tuple[str, int, str]]], bool]: |
|
1660 |
+ """Return a function to test if a matching message was emitted. |
|
1661 |
+ |
|
1662 |
+ Args: |
|
1663 |
+ level: The level to match messages at. |
|
1664 |
+ logger_name: The name of the logger to match against. |
|
1665 |
+ |
|
1666 |
+ """ |
|
1667 |
+ |
|
1668 |
+ def message_emitted( |
|
1669 |
+ text: str | re.Pattern[str], |
|
1670 |
+ record_tuples: Sequence[tuple[str, int, str]], |
|
1671 |
+ ) -> bool: |
|
1672 |
+ """Return true if a matching message was emitted. |
|
1673 |
+ |
|
1674 |
+ Args: |
|
1675 |
+ text: Substring or pattern to match against. |
|
1676 |
+ record_tuples: Items to match. |
|
1677 |
+ |
|
1678 |
+ """ |
|
1679 |
+ |
|
1680 |
+ def check_record(record: tuple[str, int, str]) -> bool: |
|
1681 |
+ if record[:2] != (logger_name, level): |
|
1682 |
+ return False |
|
1683 |
+ if isinstance(text, str): |
|
1684 |
+ return text in record[2] |
|
1685 |
+ return text.match(record[2]) is not None # pragma: no cover |
|
1686 |
+ |
|
1687 |
+ return any(map(check_record, record_tuples)) |
|
1688 |
+ |
|
1689 |
+ return message_emitted |
|
1690 |
+ |
|
1691 |
+ |
|
1692 |
+# No need to assert debug messages as of yet. |
|
1693 |
+info_emitted = message_emitted_factory(logging.INFO) |
|
1694 |
+warning_emitted = message_emitted_factory(logging.WARNING) |
|
1695 |
+deprecation_warning_emitted = message_emitted_factory( |
|
1696 |
+ logging.WARNING, logger_name=f'{cli.PROG_NAME}.deprecation' |
|
1697 |
+) |
|
1698 |
+error_emitted = message_emitted_factory(logging.ERROR) |
... | ... |
@@ -8,9 +8,11 @@ import contextlib |
8 | 8 |
import copy |
9 | 9 |
import errno |
10 | 10 |
import json |
11 |
+import logging |
|
11 | 12 |
import os |
12 | 13 |
import shutil |
13 | 14 |
import socket |
15 |
+import warnings |
|
14 | 16 |
from typing import TYPE_CHECKING, cast |
15 | 17 |
|
16 | 18 |
import click.testing |
... | ... |
@@ -201,8 +203,13 @@ for opt, config in SINGLES.items(): |
201 | 203 |
) |
202 | 204 |
|
203 | 205 |
|
204 |
-def is_harmless_config_import_warning_line(line: str) -> bool: |
|
205 |
- """Return true if the warning line is harmless, during config import.""" |
|
206 |
+def is_warning_line(line: str) -> bool: |
|
207 |
+ """Return true if the line is a warning line.""" |
|
208 |
+ return ' Warning: ' in line or ' Deprecation warning: ' in line |
|
209 |
+ |
|
210 |
+ |
|
211 |
+def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool: |
|
212 |
+ """Return true if the warning is harmless, during config import.""" |
|
206 | 213 |
possible_warnings = [ |
207 | 214 |
'Replacing invalid value ', |
208 | 215 |
'Removing ineffective setting ', |
... | ... |
@@ -215,9 +222,7 @@ def is_harmless_config_import_warning_line(line: str) -> bool: |
215 | 222 |
'because a key is also set.' |
216 | 223 |
), |
217 | 224 |
] |
218 |
- return any( # pragma: no branch |
|
219 |
- (' Warning: ' + w) in line for w in possible_warnings |
|
220 |
- ) |
|
225 |
+ return any(tests.warning_emitted(w, [record]) for w in possible_warnings) |
|
221 | 226 |
|
222 | 227 |
|
223 | 228 |
class TestCLI: |
... | ... |
@@ -492,6 +497,7 @@ class TestCLI: |
492 | 497 |
self, |
493 | 498 |
monkeypatch: pytest.MonkeyPatch, |
494 | 499 |
running_ssh_agent: tests.RunningSSHAgentInfo, |
500 |
+ caplog: pytest.LogCaptureFixture, |
|
495 | 501 |
config: _types.VaultConfig, |
496 | 502 |
) -> None: |
497 | 503 |
with monkeypatch.context(): |
... | ... |
@@ -518,13 +524,13 @@ class TestCLI: |
518 | 524 |
assert result.stderr, 'expected known error output' |
519 | 525 |
err_lines = result.stderr.splitlines(False) |
520 | 526 |
assert err_lines[0].startswith('Passphrase:') |
521 |
- assert any( # pragma: no branch |
|
522 |
- ' Warning: Setting a service passphrase is ineffective ' in line |
|
523 |
- for line in err_lines |
|
527 |
+ assert tests.warning_emitted( |
|
528 |
+ 'Setting a service passphrase is ineffective ', |
|
529 |
+ caplog.record_tuples, |
|
524 | 530 |
), 'expected known warning message' |
525 |
- assert all( # pragma: no branch |
|
526 |
- is_harmless_config_import_warning_line(line) |
|
527 |
- for line in result.stderr.splitlines(True) |
|
531 |
+ assert all(map(is_warning_line, result.stderr.splitlines(True))) |
|
532 |
+ assert all( |
|
533 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
528 | 534 |
), 'unexpected error output' |
529 | 535 |
|
530 | 536 |
@pytest.mark.parametrize( |
... | ... |
@@ -557,7 +563,7 @@ class TestCLI: |
557 | 563 |
) |
558 | 564 |
result = tests.ReadableResult.parse(_result) |
559 | 565 |
assert result.error_exit( |
560 |
- error='Error: Invalid value' |
|
566 |
+ error='Invalid value' |
|
561 | 567 |
), 'expected error exit and known error message' |
562 | 568 |
|
563 | 569 |
@pytest.mark.parametrize( |
... | ... |
@@ -624,11 +630,13 @@ class TestCLI: |
624 | 630 |
def test_211a_empty_service_name_causes_warning( |
625 | 631 |
self, |
626 | 632 |
monkeypatch: pytest.MonkeyPatch, |
633 |
+ caplog: pytest.LogCaptureFixture, |
|
627 | 634 |
) -> None: |
628 |
- def expected_warning_line(line: str) -> bool: |
|
629 |
- return is_harmless_config_import_warning_line(line) or ( |
|
630 |
- ' Warning: An empty SERVICE is not supported by vault(1)' |
|
631 |
- in line |
|
635 |
+ def is_expected_warning(record: tuple[str, int, str]) -> bool: |
|
636 |
+ return is_harmless_config_import_warning( |
|
637 |
+ record |
|
638 |
+ ) or tests.warning_emitted( |
|
639 |
+ 'An empty SERVICE is not supported by vault(1)', [record] |
|
632 | 640 |
) |
633 | 641 |
|
634 | 642 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
... | ... |
@@ -647,13 +655,13 @@ class TestCLI: |
647 | 655 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
648 | 656 |
assert result.stderr is not None, 'expected known error output' |
649 | 657 |
assert all( |
650 |
- expected_warning_line(line) |
|
651 |
- for line in result.stderr.splitlines(False) |
|
658 |
+ map(is_expected_warning, caplog.record_tuples) |
|
652 | 659 |
), 'expected known error output' |
653 | 660 |
assert cli._load_config() == { |
654 | 661 |
'global': {'length': 30}, |
655 | 662 |
'services': {}, |
656 | 663 |
}, 'requested configuration change was not applied' |
664 |
+ caplog.clear() |
|
657 | 665 |
_result = runner.invoke( |
658 | 666 |
cli.derivepassphrase_vault, |
659 | 667 |
['--import', '-'], |
... | ... |
@@ -664,8 +672,7 @@ class TestCLI: |
664 | 672 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
665 | 673 |
assert result.stderr is not None, 'expected known error output' |
666 | 674 |
assert all( |
667 |
- expected_warning_line(line) |
|
668 |
- for line in result.stderr.splitlines(False) |
|
675 |
+ map(is_expected_warning, caplog.record_tuples) |
|
669 | 676 |
), 'expected known error output' |
670 | 677 |
assert cli._load_config() == { |
671 | 678 |
'global': {'length': 30}, |
... | ... |
@@ -713,6 +720,7 @@ class TestCLI: |
713 | 720 |
def test_213_import_config_success( |
714 | 721 |
self, |
715 | 722 |
monkeypatch: pytest.MonkeyPatch, |
723 |
+ caplog: pytest.LogCaptureFixture, |
|
716 | 724 |
config: Any, |
717 | 725 |
) -> None: |
718 | 726 |
runner = click.testing.CliRunner(mix_stderr=False) |
... | ... |
@@ -735,11 +743,10 @@ class TestCLI: |
735 | 743 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
736 | 744 |
assert config2 == config, 'config not imported correctly' |
737 | 745 |
assert not result.stderr or all( # pragma: no branch |
738 |
- is_harmless_config_import_warning_line(line) |
|
739 |
- for line in result.stderr.splitlines(True) |
|
746 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
740 | 747 |
), 'unexpected error output' |
741 | 748 |
|
742 |
- @tests.hypothesis_settings_coverage_compatible |
|
749 |
+ @tests.hypothesis_settings_coverage_compatible_with_caplog |
|
743 | 750 |
@hypothesis.given( |
744 | 751 |
conf=tests.smudged_vault_test_config( |
745 | 752 |
strategies.sampled_from(TEST_CONFIGS).filter( |
... | ... |
@@ -749,6 +756,7 @@ class TestCLI: |
749 | 756 |
) |
750 | 757 |
def test_213a_import_config_success( |
751 | 758 |
self, |
759 |
+ caplog: pytest.LogCaptureFixture, |
|
752 | 760 |
conf: tests.VaultTestConfig, |
753 | 761 |
) -> None: |
754 | 762 |
config = conf.config |
... | ... |
@@ -774,8 +782,7 @@ class TestCLI: |
774 | 782 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
775 | 783 |
assert config3 == config2, 'config not imported correctly' |
776 | 784 |
assert not result.stderr or all( |
777 |
- is_harmless_config_import_warning_line(line) |
|
778 |
- for line in result.stderr.splitlines(True) |
|
785 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
779 | 786 |
), 'unexpected error output' |
780 | 787 |
|
781 | 788 |
def test_213b_import_bad_config_not_vault_config( |
... | ... |
@@ -1479,6 +1486,7 @@ contents go here |
1479 | 1486 |
def test_300_unicode_normalization_form_warning( |
1480 | 1487 |
self, |
1481 | 1488 |
monkeypatch: pytest.MonkeyPatch, |
1489 |
+ caplog: pytest.LogCaptureFixture, |
|
1482 | 1490 |
command_line: list[str], |
1483 | 1491 |
input: str | None, |
1484 | 1492 |
warning_message: str, |
... | ... |
@@ -1497,8 +1505,8 @@ contents go here |
1497 | 1505 |
) |
1498 | 1506 |
result = tests.ReadableResult.parse(_result) |
1499 | 1507 |
assert result.clean_exit(), 'expected clean exit' |
1500 |
- assert ( |
|
1501 |
- warning_message in result.stderr |
|
1508 |
+ assert tests.warning_emitted( |
|
1509 |
+ warning_message, caplog.record_tuples |
|
1502 | 1510 |
), 'expected known warning message in stderr' |
1503 | 1511 |
|
1504 | 1512 |
def test_400_missing_af_unix_support( |
... | ... |
@@ -1708,6 +1716,111 @@ Boo. |
1708 | 1716 |
assert res['kwargs'].get('err'), err_msg |
1709 | 1717 |
assert res['kwargs'].get('hide_input'), err_msg |
1710 | 1718 |
|
1719 |
+ def test_120_standard_logging_context_manager( |
|
1720 |
+ self, |
|
1721 |
+ caplog: pytest.LogCaptureFixture, |
|
1722 |
+ capsys: pytest.CaptureFixture[str], |
|
1723 |
+ ) -> None: |
|
1724 |
+ prog_name = cli.StandardCLILogging.prog_name |
|
1725 |
+ package_name = cli.StandardCLILogging.package_name |
|
1726 |
+ logger = logging.getLogger(package_name) |
|
1727 |
+ deprecation_logger = logging.getLogger(f'{package_name}.deprecation') |
|
1728 |
+ logging_cm = cli.StandardCLILogging.ensure_standard_logging() |
|
1729 |
+ with logging_cm: |
|
1730 |
+ assert ( |
|
1731 |
+ sum( |
|
1732 |
+ 1 |
|
1733 |
+ for h in logger.handlers |
|
1734 |
+ if h is cli.StandardCLILogging.cli_handler |
|
1735 |
+ ) |
|
1736 |
+ == 1 |
|
1737 |
+ ) |
|
1738 |
+ logger.warning('message 1') |
|
1739 |
+ with logging_cm: |
|
1740 |
+ deprecation_logger.warning('message 2') |
|
1741 |
+ assert ( |
|
1742 |
+ sum( |
|
1743 |
+ 1 |
|
1744 |
+ for h in logger.handlers |
|
1745 |
+ if h is cli.StandardCLILogging.cli_handler |
|
1746 |
+ ) |
|
1747 |
+ == 1 |
|
1748 |
+ ) |
|
1749 |
+ assert capsys.readouterr() == ( |
|
1750 |
+ '', |
|
1751 |
+ ( |
|
1752 |
+ f'{prog_name}: Warning: message 1\n' |
|
1753 |
+ f'{prog_name}: Deprecation warning: message 2\n' |
|
1754 |
+ ), |
|
1755 |
+ ) |
|
1756 |
+ logger.warning('message 3') |
|
1757 |
+ assert ( |
|
1758 |
+ sum( |
|
1759 |
+ 1 |
|
1760 |
+ for h in logger.handlers |
|
1761 |
+ if h is cli.StandardCLILogging.cli_handler |
|
1762 |
+ ) |
|
1763 |
+ == 1 |
|
1764 |
+ ) |
|
1765 |
+ assert capsys.readouterr() == ( |
|
1766 |
+ '', |
|
1767 |
+ f'{prog_name}: Warning: message 3\n', |
|
1768 |
+ ) |
|
1769 |
+ assert caplog.record_tuples == [ |
|
1770 |
+ (package_name, logging.WARNING, 'message 1'), |
|
1771 |
+ (f'{package_name}.deprecation', logging.WARNING, 'message 2'), |
|
1772 |
+ (package_name, logging.WARNING, 'message 3'), |
|
1773 |
+ ] |
|
1774 |
+ |
|
1775 |
+ def test_121_standard_logging_warnings_context_manager( |
|
1776 |
+ self, |
|
1777 |
+ caplog: pytest.LogCaptureFixture, |
|
1778 |
+ capsys: pytest.CaptureFixture[str], |
|
1779 |
+ ) -> None: |
|
1780 |
+ warnings_cm = cli.StandardCLILogging.ensure_standard_warnings_logging() |
|
1781 |
+ THE_FUTURE = 'the future will be here sooner than you think' # noqa: N806 |
|
1782 |
+ JUST_TESTING = 'just testing whether warnings work' # noqa: N806 |
|
1783 |
+ with warnings_cm: |
|
1784 |
+ assert ( |
|
1785 |
+ sum( |
|
1786 |
+ 1 |
|
1787 |
+ for h in logging.getLogger('py.warnings').handlers |
|
1788 |
+ if h is cli.StandardCLILogging.warnings_handler |
|
1789 |
+ ) |
|
1790 |
+ == 1 |
|
1791 |
+ ) |
|
1792 |
+ warnings.warn(UserWarning(JUST_TESTING), stacklevel=1) |
|
1793 |
+ with warnings_cm: |
|
1794 |
+ warnings.warn(FutureWarning(THE_FUTURE), stacklevel=1) |
|
1795 |
+ _out, err = capsys.readouterr() |
|
1796 |
+ err_lines = err.splitlines(True) |
|
1797 |
+ assert any( |
|
1798 |
+ f'UserWarning: {JUST_TESTING}' in line |
|
1799 |
+ for line in err_lines |
|
1800 |
+ ) |
|
1801 |
+ assert any( |
|
1802 |
+ f'FutureWarning: {THE_FUTURE}' in line |
|
1803 |
+ for line in err_lines |
|
1804 |
+ ) |
|
1805 |
+ warnings.warn(UserWarning(JUST_TESTING), stacklevel=1) |
|
1806 |
+ _out, err = capsys.readouterr() |
|
1807 |
+ err_lines = err.splitlines(True) |
|
1808 |
+ assert any( |
|
1809 |
+ f'UserWarning: {JUST_TESTING}' in line for line in err_lines |
|
1810 |
+ ) |
|
1811 |
+ assert not any( |
|
1812 |
+ f'FutureWarning: {THE_FUTURE}' in line for line in err_lines |
|
1813 |
+ ) |
|
1814 |
+ record_tuples = caplog.record_tuples |
|
1815 |
+ assert [tup[:2] for tup in record_tuples] == [ |
|
1816 |
+ ('py.warnings', logging.WARNING), |
|
1817 |
+ ('py.warnings', logging.WARNING), |
|
1818 |
+ ('py.warnings', logging.WARNING), |
|
1819 |
+ ] |
|
1820 |
+ assert f'UserWarning: {JUST_TESTING}' in record_tuples[0][2] |
|
1821 |
+ assert f'FutureWarning: {THE_FUTURE}' in record_tuples[1][2] |
|
1822 |
+ assert f'UserWarning: {JUST_TESTING}' in record_tuples[2][2] |
|
1823 |
+ |
|
1711 | 1824 |
@pytest.mark.parametrize( |
1712 | 1825 |
['command_line', 'config', 'result_config'], |
1713 | 1826 |
[ |
... | ... |
@@ -2014,7 +2127,9 @@ class TestCLITransition: |
2014 | 2127 |
cli._migrate_and_load_old_config() |
2015 | 2128 |
|
2016 | 2129 |
def test_200_forward_export_vault_path_parameter( |
2017 |
- self, monkeypatch: pytest.MonkeyPatch |
|
2130 |
+ self, |
|
2131 |
+ monkeypatch: pytest.MonkeyPatch, |
|
2132 |
+ caplog: pytest.LogCaptureFixture, |
|
2018 | 2133 |
) -> None: |
2019 | 2134 |
pytest.importorskip('cryptography', minversion='38.0') |
2020 | 2135 |
runner = click.testing.CliRunner(mix_stderr=False) |
... | ... |
@@ -2031,12 +2146,11 @@ class TestCLITransition: |
2031 | 2146 |
) |
2032 | 2147 |
result = tests.ReadableResult.parse(_result) |
2033 | 2148 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
2034 |
- assert ( |
|
2035 |
- result.stderr |
|
2036 |
- == f"""\ |
|
2037 |
-{cli.PROG_NAME}: Deprecation warning: A subcommand will be required in v1.0. See --help for available subcommands. |
|
2038 |
-{cli.PROG_NAME}: Warning: Defaulting to subcommand "vault". |
|
2039 |
-""" # noqa: E501 |
|
2149 |
+ assert tests.deprecation_warning_emitted( |
|
2150 |
+ 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2151 |
+ ) |
|
2152 |
+ assert tests.warning_emitted( |
|
2153 |
+ 'Defaulting to subcommand "vault"', caplog.record_tuples |
|
2040 | 2154 |
) |
2041 | 2155 |
assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA |
2042 | 2156 |
|
... | ... |
@@ -2056,10 +2170,12 @@ class TestCLITransition: |
2056 | 2170 |
['export'], |
2057 | 2171 |
) |
2058 | 2172 |
result = tests.ReadableResult.parse(_result) |
2059 |
- assert result.stderr.startswith(f"""\ |
|
2060 |
-{cli.PROG_NAME}: Deprecation warning: A subcommand will be required in v1.0. See --help for available subcommands. |
|
2061 |
-{cli.PROG_NAME}: Warning: Defaulting to subcommand "vault". |
|
2062 |
-""") # noqa: E501 |
|
2173 |
+ assert tests.deprecation_warning_emitted( |
|
2174 |
+ 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2175 |
+ ) |
|
2176 |
+ assert tests.warning_emitted( |
|
2177 |
+ 'Defaulting to subcommand "vault"', caplog.record_tuples |
|
2178 |
+ ) |
|
2063 | 2179 |
assert result.error_exit( |
2064 | 2180 |
error="Missing argument 'PATH'" |
2065 | 2181 |
), 'expected error exit and known error type' |
... | ... |
@@ -2068,7 +2184,10 @@ class TestCLITransition: |
2068 | 2184 |
'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol'] |
2069 | 2185 |
) |
2070 | 2186 |
def test_210_forward_vault_disable_character_set( |
2071 |
- self, monkeypatch: pytest.MonkeyPatch, charset_name: str |
|
2187 |
+ self, |
|
2188 |
+ monkeypatch: pytest.MonkeyPatch, |
|
2189 |
+ caplog: pytest.LogCaptureFixture, |
|
2190 |
+ charset_name: str, |
|
2072 | 2191 |
) -> None: |
2073 | 2192 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
2074 | 2193 |
option = f'--{charset_name}' |
... | ... |
@@ -2086,12 +2205,11 @@ class TestCLITransition: |
2086 | 2205 |
) |
2087 | 2206 |
result = tests.ReadableResult.parse(_result) |
2088 | 2207 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
2089 |
- assert ( |
|
2090 |
- result.stderr |
|
2091 |
- == f"""\ |
|
2092 |
-{cli.PROG_NAME}: Deprecation warning: A subcommand will be required in v1.0. See --help for available subcommands. |
|
2093 |
-{cli.PROG_NAME}: Warning: Defaulting to subcommand "vault". |
|
2094 |
-""" # noqa: E501 |
|
2208 |
+ assert tests.deprecation_warning_emitted( |
|
2209 |
+ 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2210 |
+ ) |
|
2211 |
+ assert tests.warning_emitted( |
|
2212 |
+ 'Defaulting to subcommand "vault"', caplog.record_tuples |
|
2095 | 2213 |
) |
2096 | 2214 |
for c in charset: |
2097 | 2215 |
assert ( |
... | ... |
@@ -2115,10 +2233,12 @@ class TestCLITransition: |
2115 | 2233 |
catch_exceptions=False, |
2116 | 2234 |
) |
2117 | 2235 |
result = tests.ReadableResult.parse(_result) |
2118 |
- assert result.stderr.startswith(f"""\ |
|
2119 |
-{cli.PROG_NAME}: Deprecation warning: A subcommand will be required in v1.0. See --help for available subcommands. |
|
2120 |
-{cli.PROG_NAME}: Warning: Defaulting to subcommand "vault". |
|
2121 |
-""") # noqa: E501 |
|
2236 |
+ assert tests.deprecation_warning_emitted( |
|
2237 |
+ 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2238 |
+ ) |
|
2239 |
+ assert tests.warning_emitted( |
|
2240 |
+ 'Defaulting to subcommand "vault"', caplog.record_tuples |
|
2241 |
+ ) |
|
2122 | 2242 |
assert result.error_exit( |
2123 | 2243 |
error='SERVICE is required' |
2124 | 2244 |
), 'expected error exit and known error type' |
... | ... |
@@ -2126,7 +2246,9 @@ class TestCLITransition: |
2126 | 2246 |
def test_300_export_using_old_config_file( |
2127 | 2247 |
self, |
2128 | 2248 |
monkeypatch: pytest.MonkeyPatch, |
2249 |
+ caplog: pytest.LogCaptureFixture, |
|
2129 | 2250 |
) -> None: |
2251 |
+ caplog.set_level(logging.INFO) |
|
2130 | 2252 |
runner = click.testing.CliRunner(mix_stderr=False) |
2131 | 2253 |
with tests.isolated_config( |
2132 | 2254 |
monkeypatch=monkeypatch, |
... | ... |
@@ -2149,16 +2271,17 @@ class TestCLITransition: |
2149 | 2271 |
) |
2150 | 2272 |
result = tests.ReadableResult.parse(_result) |
2151 | 2273 |
assert result.clean_exit(), 'expected clean exit' |
2152 |
- assert ( |
|
2153 |
- 'v0.1-style config file' in result.stderr |
|
2274 |
+ assert tests.deprecation_warning_emitted( |
|
2275 |
+ 'v0.1-style config file', caplog.record_tuples |
|
2154 | 2276 |
), 'expected known warning message in stderr' |
2155 |
- assert ( |
|
2156 |
- 'Successfully migrated to ' in result.stderr |
|
2277 |
+ assert tests.info_emitted( |
|
2278 |
+ 'Successfully migrated to ', caplog.record_tuples |
|
2157 | 2279 |
), 'expected known warning message in stderr' |
2158 | 2280 |
|
2159 | 2281 |
def test_300a_export_using_old_config_file_migration_error( |
2160 | 2282 |
self, |
2161 | 2283 |
monkeypatch: pytest.MonkeyPatch, |
2284 |
+ caplog: pytest.LogCaptureFixture, |
|
2162 | 2285 |
) -> None: |
2163 | 2286 |
runner = click.testing.CliRunner(mix_stderr=False) |
2164 | 2287 |
with tests.isolated_config( |
... | ... |
@@ -2191,11 +2314,11 @@ class TestCLITransition: |
2191 | 2314 |
) |
2192 | 2315 |
result = tests.ReadableResult.parse(_result) |
2193 | 2316 |
assert result.clean_exit(), 'expected clean exit' |
2194 |
- assert ( |
|
2195 |
- 'v0.1-style config file' in result.stderr |
|
2317 |
+ assert tests.deprecation_warning_emitted( |
|
2318 |
+ 'v0.1-style config file', caplog.record_tuples |
|
2196 | 2319 |
), 'expected known warning message in stderr' |
2197 |
- assert ( |
|
2198 |
- 'Warning: Failed to migrate to ' in result.stderr |
|
2320 |
+ assert tests.warning_emitted( |
|
2321 |
+ 'Failed to migrate to ', caplog.record_tuples |
|
2199 | 2322 |
), 'expected known warning message in stderr' |
2200 | 2323 |
|
2201 | 2324 |
|
... | ... |
@@ -118,6 +118,7 @@ class TestCLI: |
118 | 118 |
def test_301_vault_config_not_found( |
119 | 119 |
self, |
120 | 120 |
monkeypatch: pytest.MonkeyPatch, |
121 |
+ caplog: pytest.LogCaptureFixture, |
|
121 | 122 |
) -> None: |
122 | 123 |
runner = click.testing.CliRunner(mix_stderr=False) |
123 | 124 |
with tests.isolated_vault_exporter_config( |
... | ... |
@@ -132,13 +133,15 @@ class TestCLI: |
132 | 133 |
) |
133 | 134 |
result = tests.ReadableResult.parse(_result) |
134 | 135 |
assert result.error_exit( |
135 |
- error="Cannot parse 'does-not-exist.txt' as a valid config" |
|
136 |
+ error="Cannot parse 'does-not-exist.txt' as a valid config", |
|
137 |
+ record_tuples=caplog.record_tuples, |
|
136 | 138 |
), 'expected error exit and known error message' |
137 | 139 |
assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
138 | 140 |
|
139 | 141 |
def test_302_vault_config_invalid( |
140 | 142 |
self, |
141 | 143 |
monkeypatch: pytest.MonkeyPatch, |
144 |
+ caplog: pytest.LogCaptureFixture, |
|
142 | 145 |
) -> None: |
143 | 146 |
runner = click.testing.CliRunner(mix_stderr=False) |
144 | 147 |
with tests.isolated_vault_exporter_config( |
... | ... |
@@ -153,13 +156,15 @@ class TestCLI: |
153 | 156 |
) |
154 | 157 |
result = tests.ReadableResult.parse(_result) |
155 | 158 |
assert result.error_exit( |
156 |
- error="Cannot parse '.vault' as a valid config" |
|
159 |
+ error="Cannot parse '.vault' as a valid config", |
|
160 |
+ record_tuples=caplog.record_tuples, |
|
157 | 161 |
), 'expected error exit and known error message' |
158 | 162 |
assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
159 | 163 |
|
160 | 164 |
def test_403_invalid_vault_config_bad_signature( |
161 | 165 |
self, |
162 | 166 |
monkeypatch: pytest.MonkeyPatch, |
167 |
+ caplog: pytest.LogCaptureFixture, |
|
163 | 168 |
) -> None: |
164 | 169 |
runner = click.testing.CliRunner(mix_stderr=False) |
165 | 170 |
with tests.isolated_vault_exporter_config( |
... | ... |
@@ -174,13 +179,15 @@ class TestCLI: |
174 | 179 |
) |
175 | 180 |
result = tests.ReadableResult.parse(_result) |
176 | 181 |
assert result.error_exit( |
177 |
- error="Cannot parse '.vault' as a valid config" |
|
182 |
+ error="Cannot parse '.vault' as a valid config", |
|
183 |
+ record_tuples=caplog.record_tuples, |
|
178 | 184 |
), 'expected error exit and known error message' |
179 | 185 |
assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
180 | 186 |
|
181 | 187 |
def test_500_vault_config_invalid_internal( |
182 | 188 |
self, |
183 | 189 |
monkeypatch: pytest.MonkeyPatch, |
190 |
+ caplog: pytest.LogCaptureFixture, |
|
184 | 191 |
) -> None: |
185 | 192 |
runner = click.testing.CliRunner(mix_stderr=False) |
186 | 193 |
with tests.isolated_vault_exporter_config( |
... | ... |
@@ -200,7 +207,8 @@ class TestCLI: |
200 | 207 |
) |
201 | 208 |
result = tests.ReadableResult.parse(_result) |
202 | 209 |
assert result.error_exit( |
203 |
- error='Invalid vault config: ' |
|
210 |
+ error='Invalid vault config: ', |
|
211 |
+ record_tuples=caplog.record_tuples, |
|
204 | 212 |
), 'expected error exit and known error message' |
205 | 213 |
assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
206 | 214 |
|
... | ... |
@@ -152,6 +152,7 @@ class Test002CLI: |
152 | 152 |
def test_999_no_cryptography_error_message( |
153 | 153 |
self, |
154 | 154 |
monkeypatch: pytest.MonkeyPatch, |
155 |
+ caplog: pytest.LogCaptureFixture, |
|
155 | 156 |
format: str, |
156 | 157 |
config: str | bytes, |
157 | 158 |
key: str, |
... | ... |
@@ -170,5 +171,6 @@ class Test002CLI: |
170 | 171 |
) |
171 | 172 |
result = tests.ReadableResult.parse(_result) |
172 | 173 |
assert result.error_exit( |
173 |
- error=tests.CANNOT_LOAD_CRYPTOGRAPHY |
|
174 |
+ error=tests.CANNOT_LOAD_CRYPTOGRAPHY, |
|
175 |
+ record_tuples=caplog.record_tuples, |
|
174 | 176 |
), 'expected error exit and known error message' |
175 | 177 |