Merge topic branch 'the-annoying-os' into master
Marco Ricci

Marco Ricci commited on 2025-05-23 00:03:24
Zeige 2 geänderte Dateien mit 56 Einfügungen und 0 Löschungen.


* the-annoying-os:
  Exclude unused test function xfail_on_the_annoying_os from coverage
  Document the changes due to The Annoying OS in the changelog
  Add changes necessary to appease The Annoying OS
... ...
@@@ -1603,68 -1603,64 +1603,123 @@@ A cached pytest mark to skip this test 
1603 1603
  available.  Usually this means that the test targets the
1604 1604
  `derivepassphrase export vault` subcommand, whose functionality depends
1605 1605
  on cryptography support being available.
1606
+ """
1607
+ skip_if_on_the_annoying_os = pytest.mark.skipif(
1608
+     sys.platform == 'win32',
1609
+     reason='The Annoying OS behaves differently.',
1610
+ )
1611
+ """
1612
+ A cached pytest mark to skip this test if running on The Annoying
1613
+ Operating System, a.k.a. Microsoft Windows.  Usually this is due to
1614
+ unnecessary and stupid differences in the OS internals, and these
1615
+ differences are deemed irreconcilable in the context of the decorated
1616
+ test, so the test is to be skipped.
1617
+ 
1618
+ See also:
1619
+     [`xfail_on_the_annoying_os`][]
1620
+ 
1606 1621
  """
1607 1622
 +skip_if_no_multiprocessing_support = pytest.mark.skipif(
1608 1623
 +    importlib.util.find_spec('multiprocessing') is None,
1609 1624
 +    reason='no "multiprocessing" support',
1610 1625
 +)
1611 1626
 +"""
1612 1627
 +A cached pytest mark to skip this test if multiprocessing support is not
1613 1628
 +available.  Usually this means that the test targets the concurrency
1614 1629
 +features of `derivepassphrase`, which is generally only possible to test
1615 1630
 +in separate processes because the testing machinery operates on
1616 1631
 +process-global state.
1617 1632
 +"""
1618 1633
 +
1619 1634
 +MIN_CONCURRENCY = 4
1620 1635
 +"""
1621 1636
 +The minimum amount of concurrent threads used for testing.
1622 1637
 +"""
1623 1638
 +
1624 1639
 +
1625 1640
 +def get_concurrency_limit() -> int:
1626 1641
 +    """Return the imposed limit on the number of concurrent threads.
1627 1642
 +
1628 1643
 +    We use [`os.process_cpu_count`][] as the limit on Python 3.13 and
1629 1644
 +    higher, and [`os.cpu_count`][] on Python 3.12 and below.  On
1630 1645
 +    Python 3.12 and below, we explicitly support the `PYTHON_CPU_COUNT`
1631 1646
 +    environment variable.  We guarantee at least [`MIN_CONCURRENCY`][]
1632 1647
 +    many threads in any case.
1633 1648
 +
1634 1649
 +    """  # noqa: RUF002
1635 1650
 +    result: int | None = None
1636 1651
 +    if sys.version_info >= (3, 13):
1637 1652
 +        result = os.process_cpu_count()
1638 1653
 +    else:
1639 1654
 +        try:
1640 1655
 +            cpus = os.sched_getaffinity(os.getpid())
1641 1656
 +        except AttributeError:
1642 1657
 +            pass
1643 1658
 +        else:
1644 1659
 +            result = len(cpus)
1645 1660
 +    return max(result if result is not None else 0, MIN_CONCURRENCY)
1646 1661
 +
1647 1662
 +
1648 1663
 +def get_concurrency_step_count(
1649 1664
 +    settings: hypothesis.settings | None = None,
1650 1665
 +) -> int:
1651 1666
 +    """Return the desired step count for concurrency-related tests.
1652 1667
 +
1653 1668
 +    This is the smaller of the [general concurrency
1654 1669
 +    limit][tests.get_concurrency_limit] and the step count from the
1655 1670
 +    current hypothesis settings.
1656 1671
 +
1657 1672
 +    Args:
1658 1673
 +        settings:
1659 1674
 +            The hypothesis settings for a specific tests.  If not given,
1660 1675
 +            then the current profile will be queried directly.
1661 1676
 +
1662 1677
 +    """
1663 1678
 +    if settings is None:  # pragma: no cover
1664 1679
 +        settings = hypothesis.settings()
1665 1680
 +    return min(get_concurrency_limit(), settings.stateful_step_count)
1666 1681
  
1667 1682
  
1683
+ def xfail_on_the_annoying_os(
1684
+     f: Callable | None = None,
1685
+     /,
1686
+     *,
1687
+     reason: str = '',
1688
+ ) -> pytest.MarkDecorator | Any:  # pragma: no cover
1689
+     """Annotate a test which fails on The Annoying OS.
1690
+ 
1691
+     Annotate a test to indicate that it fails on The Annoying Operating
1692
+     System, a.k.a. Microsoft Windows.  Usually this is due to
1693
+     differences in the design of OS internals, and usually, these
1694
+     differences are both unnecessary and stupid.
1695
+ 
1696
+     Args:
1697
+         f:
1698
+             A callable to decorate.  If not given, return the pytest
1699
+             mark directly.
1700
+         reason:
1701
+             An optional, more detailed reason stating why this test
1702
+             fails on The Annoying OS.
1703
+ 
1704
+     Returns:
1705
+         The callable, marked as an expected failure on the Annoying OS,
1706
+         or alternatively a suitable pytest mark if no callable was
1707
+         passed.  The reason will begin with the phrase "The Annoying OS
1708
+         behaves differently.", and the optional detailed reason, if not
1709
+         empty, will follow.
1710
+ 
1711
+     """
1712
+     base_reason = 'The Annoying OS behaves differently.'
1713
+     full_reason = base_reason if not reason else f'{base_reason}  {reason}'
1714
+     mark = pytest.mark.xfail(
1715
+         sys.platform == 'win32',
1716
+         reason=full_reason,
1717
+         raises=(AssertionError, hypothesis.errors.FailedHealthCheck),
1718
+         strict=True,
1719
+     )
1720
+     return mark if f is None else mark(f)
1721
+ 
1722
+ 
1668 1723
  def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]:
1669 1724
      """Return a list of all SSH test keys, as key/comment pairs.
1670 1725
  
... ...
@@@ -12,9 -12,9 +12,10 @@@ import errn
12 12
  import io
13 13
  import json
14 14
  import logging
15
+ import operator
15 16
  import os
16 17
  import pathlib
17 18
 +import queue
18 19
  import re
19 20
  import shlex
20 21
  import shutil
21 22