pg-embed-setup-unpriv 0.6.3

Initializes postgresql_embedded clusters with platform-appropriate setup
Documentation
"""The values the timeout contract compares, and the terms it derives.

Separated from the nextest reading in ``nextest_budgets`` and the
workflow reading in ``coverage_lanes`` so each stays legible on its own
and none outgrows the 400-line limit ``AGENTS.md`` sets.

See "Test timeouts: four tiers, outermost last" in
``docs/developers-guide.md``.
"""

import collections.abc as cabc
import typing as typ
from pathlib import Path

from fractions import Fraction

from nextest_durations import DurationGrammarError
from nextest_durations import read_exact as read_duration

REPO_ROOT: typ.Final[Path] = Path(__file__).resolve().parents[2]
WORKFLOWS_DIRECTORY: typ.Final[Path] = REPO_ROOT / ".github" / "workflows"
NEXTEST_CONFIG: typ.Final[Path] = REPO_ROOT / ".config" / "nextest.toml"

#: The environment variable the shared coverage action reads for its
#: wall-clock cap on one `cargo` invocation.
WATCHDOG_VARIABLE: typ.Final[str] = "RUN_RUST_CARGO_WAIT_TIMEOUT"

#: The action whose steps run under that watchdog.
COVERAGE_ACTION: typ.Final[str] = (
    "leynos/shared-actions/.github/actions/generate-coverage"
)

#: Everything in a coverage job that is not a `cargo` invocation the
#: watchdog bounds: the checkout and toolchain setup, the cache save and
#: restore, and here also the suite's own `cargo nextest` step and the
#: Loom models, which run outside the coverage step. The job timer
#: covers all of it; the watchdog does not.
#:
#: Measured from the worst of many runs rather than one, and across runs
#: of every conclusion rather than successful ones only. Across the last
#: 115 `ci.yml` coverage jobs, 44 successful, 55 failed, and 16
#: cancelled, the widest gap between the coverage step and its job was
#: 969 s on run 30024924292. Across all 19 `coverage-main.yml` runs it
#: was 42 s on run 29354687551. No cancelled run came near the ceiling,
#: the worst reaching 727 s of a 3,600 s budget, so none was ended by
#: any of these four timers. Twenty minutes covers the worst gap seen
#: with eleven minutes to spare, and none of those runs was genuinely
#: cold.
OUTSIDE_WATCHDOG_ALLOWANCE_SECONDS: typ.Final[Fraction] = Fraction(20 * 60)

#: The watchdog every coverage step is required to declare, as a value
#: rather than as a floor.
#:
#: The ordering contract elsewhere derives a floor from the nextest
#: budgets and asserts each watchdog clears it. That is what catches a
#: watchdog set too low, and it is not a substitute for this: a floor is
#: satisfied by any larger number, so a lane raised to six hours by a
#: paste would pass every derived comparison while quietly letting a
#: wedged `cargo` burn the job's whole ceiling. This constant says what
#: the value is. The two together mean a change has to be deliberate in
#: both places and still has to clear the arithmetic.
#:
#: 1,800 s is also the shared action's own default, which is why writing
#: it down matters: deleting the variable changes nothing observable
#: until the run it kills.
DECLARED_WATCHDOG_SECONDS: typ.Final[Fraction] = Fraction(30 * 60)

#: What nextest allows a test between `SIGTERM` and `SIGKILL` when a
#: `slow-timeout` table names no `grace-period`. The default is per
#: declaration rather than per file: a table omitting the field gets
#: this, whatever another table names. Every table in this repository's
#: `.config/nextest.toml` names 5 s, so the value in force here is five
#: seconds and this is what a new table omitting the field would allow.
NEXTEST_DEFAULT_GRACE_PERIOD_SECONDS: typ.Final[Fraction] = Fraction(10)

#: Added to that grace period to cover the teardown and report writing
#: that follow it. Kept a separate term rather than folded into a single
#: floor, so raising the grace period raises the requirement instead of
#: being absorbed silently.
TERMINATION_SAFETY_MARGIN_SECONDS: typ.Final[Fraction] = Fraction(60)

#: Build time inside a `cargo` invocation before nextest starts its own
#: clock.
COLD_BUILD_ALLOWANCE_SECONDS: typ.Final[Fraction] = Fraction(10 * 60)

#: The whole-run budget `.config/nextest.toml` must declare, as
#: `docs/developers-guide.md` records it. Pinned as well as ordered: the
#: ordering below holds for a wide range of values and would not notice
#: the budget drifting away from the guide.
REQUIRED_GLOBAL_TIMEOUT_SECONDS: typ.Final[Fraction] = Fraction(10 * 60)

#: How far a ceiling must sit above the sum it contains, rather than
#: merely reaching it. A ceiling equal to that sum cancels the job at
#: the moment the watchdog would have reported the overrun, and the
#: report is the only thing that makes an overrun actionable.
CEILING_MARGIN_SECONDS: typ.Final[Fraction] = Fraction(15 * 60)

#: The ceiling every coverage job must carry, likewise from the guide.
#: The requirement is 50 minutes, the margin takes it to 65, and the
#: estate's comparison is strict, so the ceiling is the next whole
#: minute above that: a ceiling equal to the requirement it contains
#: cancels the job at the moment the watchdog would have reported.
REQUIRED_JOB_CEILING_SECONDS: typ.Final[Fraction] = Fraction(66 * 60)


class TimeoutBudgetError(ValueError):
    """Raised when a configured budget cannot be read as a bound.

    Attributes
    ----------
    field : str
        The configuration key at fault.
    value : object
        What the configuration said, so the message names the text an
        author has to change rather than only the rule it broke.
    """

    def __init__(self, message: str, *, field: str, value: object) -> None:
        """Record the failing key and its value alongside the message.

        Parameters
        ----------
        message : str
            The human-readable explanation.
        field : str
            The configuration key at fault.
        value : object
            What the configuration said.
        """
        super().__init__(message)
        self.field = field
        self.value = value


class NextestConfigurationError(TimeoutBudgetError):
    """Raised when the nextest configuration cannot be read at all.

    Separate from a budget that reads as unbounded. A file that is not
    TOML, a ``slow-timeout`` table with no ``period``, or a file with no
    ``slow-timeout`` anywhere, is a configuration this contract cannot
    reason about rather than one whose tiers are in the wrong order.
    """


class UnboundedTestError(TimeoutBudgetError):
    """Raised when a ``slow-timeout`` terminates no test.

    ``terminate-after`` is optional, and without it nextest marks a test
    slow and lets it run on, so the configuration parses, reads as
    deliberate, and bounds nothing. Reporting that as a period-long
    budget would put a number on the tier that is missing.
    """


def mapping_or_empty(value: object) -> dict[str, object]:
    """Return a YAML value as a mapping, or an empty one.

    Structural matching rather than a chain of `isinstance` checks, per
    this repository's own guidance, and with an explicit fallback so a
    scalar or a list where a mapping was expected yields nothing rather
    than raising from an attribute access several frames away.

    Parameters
    ----------
    value : object
        Any value the YAML parser produced.

    Returns
    -------
    dict[str, object]
        The mapping, or an empty one when the value is not one.
    """
    match value:
        case dict() as mapping:
            return typ.cast("dict[str, object]", mapping)
        case _:
            return {}


def sequence_or_empty(value: object) -> list[object]:
    """Return a YAML value as a list, or an empty one.

    Parameters
    ----------
    value : object
        Any value the YAML parser produced.

    Returns
    -------
    list[object]
        The list, or an empty one when the value is not one.
    """
    match value:
        case list() as items:
            return typ.cast("list[object]", items)
        case _:
            return []


def required_ceiling(
    budgets: cabc.Sequence[Fraction], allowance: Fraction
) -> Fraction:
    """Return the smallest acceptable ceiling for one job, in seconds.

    Three terms. Each coverage step may legitimately spend its whole
    watchdog, so the sum is the floor. The measured work outside those
    windows is added because the job timer covers it and the watchdogs
    do not. The margin is added because a ceiling equal to that sum
    cancels the job at the moment the watchdog would have reported the
    overrun, and the report is the only thing that makes an overrun
    actionable.

    Parameters
    ----------
    budgets : cabc.Sequence[Fraction]
        One watchdog budget per coverage step in the job.
    allowance : Fraction
        The measured work outside those windows, in seconds.

    Returns
    -------
    Fraction
        The smallest acceptable ceiling, in seconds, exactly. Every term
        is a ``Fraction`` so that the sum stays exact: a single ``float``
        among them would convert the whole ceiling back, and the two
        comparisons that read it are the ones a lost second would pass.
    """
    return sum(budgets, Fraction(0)) + allowance + CEILING_MARGIN_SECONDS

def seconds(duration: str) -> Fraction:
    """Convert a nextest duration to seconds.

    The grammar and the arithmetic live in ``nextest_durations``, which
    mirrors ``humantime`` 2.3.0 exactly: integer seconds and
    nanoseconds, kept apart the way ``humantime`` keeps them, with every
    intermediate checked against the range of a ``u64`` and a fraction
    refused unless it divides into its unit exactly. This wrapper exists
    so the refusal arrives as the `NextestConfigurationError` the rest
    of these readings report faults with, rather than as a second error
    type callers would have to catch.

    Parameters
    ----------
    duration : str
        A duration as nextest spells it, such as ``"120s"``, the
        multi-component ``"1m 30s"`` or the fractional ``"1.5m"``.

    Returns
    -------
    Fraction
        The duration in seconds, exactly.

    Raises
    ------
    NextestConfigurationError
        If the text is not a duration nextest would accept.

    The value is exact rather than a float. Above two to the fifty-third
    a float no longer holds every integer second, so two budgets nextest
    reads as different would compare equal and an ordering that must
    hold strictly would pass on a configuration that violates it.

    Examples
    --------
    >>> seconds("120s") == 120
    True
    >>> seconds("1m 30s") == 90
    True
    >>> seconds("1.5m") == 90
    True
    """
    try:
        return read_duration(duration)
    except DurationGrammarError as error:
        raise NextestConfigurationError(
            str(error), field="duration", value=duration
        ) from error