blob: c54c449d5c9d716e7a79521dd1fdb66261c53078 [file]
"""Test-facing helpers for translated httpd tests.
Phase 2 exposes a small, pytest-idiomatic surface that the translated ``.t``
files use. The Perl Apache::Test vocabulary maps as follows:
* ``plan tests => N`` -> dropped (pytest counts tests itself)
* ``ok $cond`` -> ``assert cond``
* ``ok t_cmp($got, $exp, $d)`` -> ``assert t_cmp(got, exp), d`` (see :func:`t_cmp`)
* ``need_module 'x'`` (in plan) -> ``@need_module("x")`` marker on the test
* ``have_min_apache_version()`` -> :func:`have_min_apache_version` (runtime gate)
* ``GET`` / ``GET_BODY`` / ... -> methods on the ``http`` fixture (client.py)
The requirement markers (need_*) are evaluated at collection time by a
``pytest_collection_modifyitems`` hook in conftest.py, which probes the target
httpd once and skips tests whose requirements aren't met -- the analog of
Apache::Test's plan-time skipping.
"""
from __future__ import annotations
import re
from typing import Any
import pytest
# --------------------------------------------------------------------------- #
# Assertions / comparison
# --------------------------------------------------------------------------- #
def t_cmp(received: Any, expected: Any) -> bool:
"""Faithful port of Apache::TestUtil::t_cmp / t_is_equal.
* If ``expected`` is a compiled regex (``re.Pattern``), return whether it
matches ``received`` anywhere (Perl ``=~``, i.e. ``re.search``).
* Lists/tuples compare element-wise; dicts compare key/value-wise.
* ``None`` equals only ``None``.
* Otherwise compare by string equality, mirroring Perl's ``eq`` (both sides
stringified) so ``200 == "200"`` holds as in the Perl tests.
Returns a bool so callers write ``assert t_cmp(got, exp), "desc"`` -- the
description becomes the assertion message (the Perl 3rd arg / t_debug).
"""
if isinstance(expected, re.Pattern):
return expected.search(str(received)) is not None
if isinstance(expected, (list, tuple)) and isinstance(received, (list, tuple)):
return len(received) == len(expected) and all(
t_cmp(r, e) for r, e in zip(received, expected)
)
if isinstance(expected, dict) and isinstance(received, dict):
return received.keys() == expected.keys() and all(
t_cmp(received[k], expected[k]) for k in expected
)
if received is None or expected is None:
return received is None and expected is None
# Perl `eq` is a string comparison; stringify both sides.
return str(received) == str(expected)
# --------------------------------------------------------------------------- #
# Requirement markers (need_*) -- thin wrappers over pytest.mark
# --------------------------------------------------------------------------- #
# Each marker records its requirement as marker args; the collection hook in
# conftest.py reads them against the probed HttpdInfo and skips as needed.
def need_module(*names: str) -> pytest.MarkDecorator:
"""Skip unless every named httpd module is available (need_module)."""
return pytest.mark.need_module(*names)
def need_min_apache_version(version: str) -> pytest.MarkDecorator:
"""Skip unless the server is at least ``version`` (need_min_apache_version)."""
return pytest.mark.need_min_apache_version(version)
def need_cgi() -> pytest.MarkDecorator:
"""Skip unless a CGI module (mod_cgi or mod_cgid) is available (need_cgi)."""
return pytest.mark.need_cgi()
def need_php() -> pytest.MarkDecorator:
"""Skip unless a PHP SAPI module is available (need_php)."""
return pytest.mark.need_php()
def need_ssl() -> pytest.MarkDecorator:
"""Skip unless mod_ssl is available (need_ssl)."""
return pytest.mark.need_ssl()
def need_lwp() -> pytest.MarkDecorator:
"""Always satisfied (the Perl need_lwp gated on the LWP client being present;
the Python client -- httpx -- is always available)."""
return pytest.mark.need_lwp()