mirror of
https://github.com/apache/httpd.git
synced 2026-10-23 22:49:01 +03:00
Submitted by Maya Echtinaw This closes #808 git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1939059 13f79535-47bb-0310-9956-ffa450edef68
309 lines
10 KiB
Python
309 lines
10 KiB
Python
"""Self-test for the optional-dependency gating in pyhttpd/depends.py.
|
|
|
|
Runs a synthetic test file in a pytester subprocess, with and without
|
|
--strict-optional, and asserts skipped-vs-failed outcomes. The subprocess
|
|
loads the real plugin with '-p pyhttpd.depends' and nothing else, so this
|
|
needs no built httpd, no config.ini and no network.
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from pyhttpd.env import HttpdTestEnv
|
|
|
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
pytest_plugins = ["pytester"]
|
|
|
|
|
|
class _StubErrorLog:
|
|
"""Enough of HttpdErrorLog for the autouse _package_scope fixture."""
|
|
|
|
def clear_ignored_matches(self):
|
|
pass
|
|
|
|
def clear_ignored_lognos(self):
|
|
pass
|
|
|
|
def add_ignored_lognos(self, lognos):
|
|
pass
|
|
|
|
|
|
class _StubEnv:
|
|
"""Satisfies the autouse fixtures in test/conftest.py.
|
|
|
|
Those are written for a test that drives a real httpd; this module
|
|
drives a pytester subprocess instead, so every one of them is a no-op
|
|
here. apache_stop() must report success, _package_scope asserts on it.
|
|
"""
|
|
|
|
httpd_error_log = _StubErrorLog()
|
|
|
|
def set_current_test_name(self, name):
|
|
pass
|
|
|
|
def check_error_log(self):
|
|
pass
|
|
|
|
def apache_stop(self):
|
|
return 0
|
|
|
|
|
|
@pytest.fixture(scope="package")
|
|
def env():
|
|
return _StubEnv()
|
|
|
|
|
|
SYNTHETIC = """
|
|
import pytest
|
|
from pyhttpd.depends import needs_dependency
|
|
|
|
# dependency missing: skip by default, fail when strict
|
|
@needs_dependency("frobnicator", False,
|
|
reason="frobnicator >= 2.0 not installed",
|
|
detected="frobnicator 1.0 at /usr/bin/frobnicator")
|
|
def test_dep_missing():
|
|
assert False, "must never run"
|
|
|
|
# dependency present: always runs
|
|
@needs_dependency("widget", True, reason="widget not installed")
|
|
def test_dep_present():
|
|
assert True
|
|
|
|
# not applicable: skips in both modes
|
|
@pytest.mark.skipif(True, reason="httpd too old")
|
|
def test_not_applicable():
|
|
assert False, "must never run"
|
|
|
|
# both kinds of gate on one test: "not applicable" wins, even when strict
|
|
@pytest.mark.skipif(True, reason="STRESS_TEST not set in env")
|
|
@needs_dependency("frobnicator", False, reason="frobnicator not installed")
|
|
def test_not_applicable_beats_missing_dep():
|
|
assert False, "must never run"
|
|
|
|
# an inactive skipif must not shield a missing dependency
|
|
@pytest.mark.skipif(False, reason="httpd too old")
|
|
@needs_dependency("frobnicator", False, reason="frobnicator not installed")
|
|
def test_inactive_skipif_does_not_shield():
|
|
assert False, "must never run"
|
|
|
|
# two dependencies missing at once, both should be reported
|
|
@needs_dependency("OCSP responder", False, reason="no OCSP responder")
|
|
@needs_dependency("mod_ssl", False, reason="no mod_ssl available")
|
|
class TestStacked:
|
|
def test_stacked(self):
|
|
assert False, "must never run"
|
|
|
|
# a class-level marker must reach the method
|
|
@needs_dependency("a2md", False, reason="no a2md available")
|
|
class TestClassLevel:
|
|
def test_class_level(self):
|
|
assert False, "must never run"
|
|
"""
|
|
|
|
MODULE_LEVEL = """
|
|
import pytest
|
|
from pyhttpd.depends import needs_dependency
|
|
|
|
# module-level pytestmark, which a per-decorator scan would not see at all
|
|
pytestmark = [needs_dependency("nghttp", False, reason="nghttp not available")]
|
|
|
|
def test_module_gated():
|
|
assert False, "must never run"
|
|
"""
|
|
|
|
LAZY = """
|
|
from pyhttpd.depends import needs_dependency
|
|
|
|
CALLS = []
|
|
|
|
def probe():
|
|
CALLS.append(1)
|
|
return False
|
|
|
|
@needs_dependency("lazy", probe, reason="lazy dep missing")
|
|
def test_lazy():
|
|
assert False, "must never run"
|
|
|
|
def test_probe_ran_once():
|
|
assert CALLS == [1]
|
|
"""
|
|
|
|
# test_dep_present is the only one that runs; of the rest, test_dep_missing,
|
|
# TestStacked and TestClassLevel are dependency gates and the other three
|
|
# are "not applicable" or shielded.
|
|
ALL_SKIPPED = dict(passed=1, skipped=6)
|
|
STRICT_SPLIT = dict(passed=1, skipped=2, errors=4)
|
|
|
|
|
|
@pytest.fixture
|
|
def synth(pytester, monkeypatch):
|
|
# never inherit strict mode from the outer run
|
|
monkeypatch.delenv("PYHTTPD_STRICT_OPTIONAL", raising=False)
|
|
# let the subprocess import pyhttpd.depends
|
|
monkeypatch.setenv("PYTHONPATH", os.pathsep.join(
|
|
[TEST_DIR, os.environ.get("PYTHONPATH", "")]).rstrip(os.pathsep))
|
|
pytester.makepyfile(test_synthetic=SYNTHETIC, test_modlevel=MODULE_LEVEL,
|
|
test_lazy=LAZY)
|
|
return pytester
|
|
|
|
|
|
def _run(pytester, *args):
|
|
return pytester.runpytest_subprocess("-p", "pyhttpd.depends",
|
|
"test_synthetic.py", *args)
|
|
|
|
|
|
def test_flag_is_advertised(synth):
|
|
r = synth.runpytest_subprocess("-p", "pyhttpd.depends", "--help")
|
|
r.stdout.fnmatch_lines(["*--strict-optional*"])
|
|
|
|
|
|
def test_default_is_off_and_missing_dep_skips(synth):
|
|
r = _run(synth)
|
|
r.assert_outcomes(**ALL_SKIPPED)
|
|
assert r.ret == 0
|
|
|
|
|
|
def test_default_skip_keeps_the_original_reason(synth):
|
|
r = _run(synth, "-rs")
|
|
r.stdout.fnmatch_lines(["*frobnicator >= 2.0 not installed*"])
|
|
r.stdout.fnmatch_lines(["*no a2md available*"])
|
|
|
|
|
|
def test_strict_flag_fails_on_missing_dep(synth):
|
|
r = _run(synth, "--strict-optional")
|
|
r.assert_outcomes(**STRICT_SPLIT)
|
|
assert r.ret != 0
|
|
r.stdout.fnmatch_lines([
|
|
"*[[]strict-optional[]] missing optional dependency: frobnicator*",
|
|
"*reason:*frobnicator >= 2.0 not installed*",
|
|
"*detected: frobnicator 1.0 at /usr/bin/frobnicator*",
|
|
"*drop*--strict-optional*",
|
|
])
|
|
|
|
|
|
def test_strict_reports_every_missing_dep(synth):
|
|
r = _run(synth, "--strict-optional")
|
|
r.stdout.fnmatch_lines(["*missing optional dependency: OCSP responder*"])
|
|
r.stdout.fnmatch_lines(["*missing optional dependency: mod_ssl*"])
|
|
|
|
|
|
def test_not_applicable_still_skips_when_strict(synth):
|
|
r = _run(synth, "--strict-optional", "-rs")
|
|
# a plain skipif carrying no dependency marker at all
|
|
r.stdout.fnmatch_lines(["*httpd too old*"])
|
|
# and one stacked on a missing dependency: the skipif wins, and the
|
|
# reason reported is its own, not the dependency's
|
|
r.stdout.fnmatch_lines(["*STRESS_TEST not set in env*"])
|
|
|
|
|
|
def test_env_var_enables_strict(synth, monkeypatch):
|
|
monkeypatch.setenv("PYHTTPD_STRICT_OPTIONAL", "1")
|
|
r = _run(synth)
|
|
r.assert_outcomes(**STRICT_SPLIT)
|
|
assert r.ret != 0
|
|
|
|
|
|
@pytest.mark.parametrize("val", ["", "0", "false", "no", "off"])
|
|
def test_env_var_falsey_values_keep_strict_off(synth, monkeypatch, val):
|
|
monkeypatch.setenv("PYHTTPD_STRICT_OPTIONAL", val)
|
|
r = _run(synth)
|
|
r.assert_outcomes(**ALL_SKIPPED)
|
|
assert r.ret == 0
|
|
|
|
|
|
def test_module_level_pytestmark_is_honoured(synth):
|
|
r = synth.runpytest_subprocess("-p", "pyhttpd.depends", "test_modlevel.py")
|
|
r.assert_outcomes(skipped=1)
|
|
r = synth.runpytest_subprocess("-p", "pyhttpd.depends", "test_modlevel.py",
|
|
"--strict-optional")
|
|
r.assert_outcomes(errors=1)
|
|
|
|
|
|
def test_callable_available_is_evaluated_once_at_collection(synth):
|
|
r = synth.runpytest_subprocess("-p", "pyhttpd.depends", "test_lazy.py")
|
|
r.assert_outcomes(passed=1, skipped=1)
|
|
|
|
|
|
# --- env.require(), the in-test counterpart of the decorator ---------------
|
|
#
|
|
# Exercised against a stand-in rather than a real HttpdTestEnv, which would
|
|
# need a built httpd and a running server just to reach the one branch that
|
|
# matters here.
|
|
|
|
class _RequireEnv:
|
|
require = HttpdTestEnv.require
|
|
|
|
def __init__(self, strict):
|
|
self._strict_optional = strict
|
|
self.current_test_name = "unit::require"
|
|
|
|
|
|
def test_require_is_a_noop_when_the_dependency_is_present():
|
|
_RequireEnv(strict=False).require(True, "need curl")
|
|
_RequireEnv(strict=True).require(True, "need curl")
|
|
|
|
|
|
def test_require_skips_when_not_strict():
|
|
with pytest.raises(BaseException) as exc:
|
|
_RequireEnv(strict=False).require(
|
|
False, "need at least curl v8.0.0 for this",
|
|
dep="curl", detected="curl 7.88.1")
|
|
assert exc.value.__class__.__name__ == "Skipped"
|
|
assert "need at least curl v8.0.0 for this" in str(exc.value.args[0])
|
|
|
|
|
|
def test_require_reports_the_dependency_name_not_the_reason():
|
|
"""dep is what is missing; reason is why the test wanted it."""
|
|
with pytest.raises(BaseException) as exc:
|
|
_RequireEnv(strict=True).require(False, "no mod_ssl available")
|
|
# no dep= given, so the reason stands in for the name
|
|
assert "missing optional dependency: no mod_ssl available" in str(exc.value.args[0])
|
|
|
|
|
|
# --- the tool probes -------------------------------------------------------
|
|
#
|
|
# config.ini always names curl/nghttp/h2load, so the name alone says nothing
|
|
# about whether the tool is installed. Probing used to raise FileNotFoundError
|
|
# out of collection; it must report "absent" instead.
|
|
|
|
class _ProbeEnv:
|
|
"""Drives the curl probe without a config.ini or a built httpd."""
|
|
|
|
_run_version = staticmethod(HttpdTestEnv._run_version)
|
|
_versiontuple = HttpdTestEnv._versiontuple
|
|
_probe_curl = HttpdTestEnv._probe_curl
|
|
has_curl = HttpdTestEnv.has_curl
|
|
curl_version_str = HttpdTestEnv.curl_version_str
|
|
curl_is_at_least = HttpdTestEnv.curl_is_at_least
|
|
curl_is_less_than = HttpdTestEnv.curl_is_less_than
|
|
curl_is_8_1_x = HttpdTestEnv.curl_is_8_1_x
|
|
|
|
def __init__(self, curl):
|
|
self._curl = curl
|
|
self._curl_version = None
|
|
|
|
|
|
@pytest.mark.parametrize("curl", ["/nonexistent/curl", "curl-does-not-exist", ""])
|
|
def test_absent_curl_probes_as_missing_rather_than_raising(curl):
|
|
env = _ProbeEnv(curl)
|
|
assert env.has_curl() is False
|
|
assert env.curl_version_str() is None
|
|
# every comparison answers False rather than blowing up collection
|
|
assert env.curl_is_at_least('8.0.0') is False
|
|
assert env.curl_is_less_than('8.0.0') is False
|
|
assert env.curl_is_8_1_x() is False
|
|
|
|
|
|
def test_require_fails_when_strict_and_names_the_dependency():
|
|
with pytest.raises(BaseException) as exc:
|
|
_RequireEnv(strict=True).require(
|
|
False, "need at least curl v8.0.0 for this",
|
|
dep="curl", detected="curl 7.88.1")
|
|
assert exc.value.__class__.__name__ == "Failed"
|
|
msg = str(exc.value.args[0])
|
|
assert "[strict-optional] missing optional dependency: curl" in msg
|
|
assert "reason: need at least curl v8.0.0 for this" in msg
|
|
assert "detected: curl 7.88.1" in msg
|
|
assert "unit::require" in msg
|