Source code for tests.conftest

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Fixtures, configuration and helpers for tests."""

from __future__ import annotations

import os

import click
import pytest
import requests
from extra_platforms.pytest import skip_windows

from click_extra.color import COLOR_ENVVARS
from click_extra.pytest import (  # noqa: F401
    assert_output_regex,
    create_config,
    invoke,
    runner,
)
from click_extra.theme import THEME_ENVVAR

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator


[docs] @pytest.fixture(scope="session") def httpserver_listen_address(): """Bind the local HTTP server to the loopback address, not to a name. Overrides ``pytest-httpserver``'s default of ``("localhost", 0)``. Resolving that name is the one thing the ``tests/test_config.py`` cases serving a configuration file over HTTP need from the host, and a build sandbox is exactly where it is unavailable: the Nix one on macOS denies the lookup, so every one of them errors out with ``socket.gaierror: [Errno 8] nodename nor servname provided, or not known``. Binding the literal address asks nothing of the resolver. Whether a sandbox additionally gates the loopback socket is its own policy, and a separate question: this only removes the lookup that failed first. """ return ("127.0.0.1", 0)
@pytest.fixture(autouse=True) def _isolate_color_envvars(): """Remove output-affecting environment variables so tests are deterministic. Variables like ``NO_COLOR`` and ``LLM`` are commonly set by shells, editors, and AI tooling. Their presence overrides ``ColorOption``'s default, making color-dependent tests fail in developer environments. ``ACCESSIBLE`` is isolated for the same reason: it lowers the ``--color`` and ``--table-format`` defaults. ``CLICK_EXTRA_THEME`` is meant to be exported from a shell profile, so a developer running the suite is precisely who is likely to have it set, and it would repaint every help screen the assertions pin. """ isolated = (*COLOR_ENVVARS, "ACCESSIBLE", THEME_ENVVAR) saved = {var: os.environ.pop(var) for var in isolated if var in os.environ} yield os.environ.update(saved) skip_windows_colors = skip_windows(reason="Click overstrip colors on Windows") """Skips color tests on Windows as ``click.testing.invoke`` overzealously strips colors. See: - https://github.com/pallets/click/issues/2111 - https://github.com/pallets/click/issues/2110 """ #: Status codes a host answers with when it is refusing *this* request rather #: than reporting something about the resource: a rate limit, a proxy hiccup, #: or a service that is briefly down. TRANSIENT_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504})
[docs] def fetch_or_skip(url: str, timeout: float = 60) -> requests.Response: """Fetch `url`, skipping the test when the failure says nothing about it. A network-dependent test asserts something about what a host serves. It cannot assert it while the host is rate-limiting, timing out, or down, and a bare `assert response.ok` there reports `assert False`: a red run that looks like the finding the test exists to make, with nothing naming the cause. GitHub throttles anonymous archive downloads, so a full-suite run hits this on its own schedule and reads as an order-dependent flake. A response that *is* about the resource still fails, loudly and with its status: a 404 means the URL these tests build no longer resolves, which is the finding, not the weather. """ try: response = requests.get(url, timeout=timeout) except requests.RequestException as error: pytest.skip(f"cannot reach {url}: {error}") if response.status_code in TRANSIENT_STATUS_CODES: pytest.skip(f"{url} answered {response.status_code} {response.reason}") assert response.ok, f"{url} answered {response.status_code} {response.reason}" return response
[docs] def walk_commands( command: click.Command, ctx: click.Context | None = None, path: tuple[str, ...] = (), ) -> Iterator[tuple[tuple[str, ...], click.Command]]: """Yield every `(path, command)` pair under `command`, itself included. `path` holds the subcommand names leading to the command, the root's own name excluded, so joining it names the invocation a user would type. :param command: the command to walk, a group or a leaf. :param ctx: the context `command` is looked up in. Built from the command itself when omitted, which is what a walk starting at the root wants. :param path: the names already walked through, for the recursion. """ if ctx is None: ctx = click.Context(command, info_name=command.name) yield path, command if isinstance(command, click.Group): for name in command.list_commands(ctx): sub = command.get_command(ctx, name) if sub is None: continue sub_ctx = click.Context(sub, parent=ctx, info_name=name) yield from walk_commands(sub, sub_ctx, (*path, name))