blob: 003ecfa1d6fe115a2a564104ff1eaf8d36171386 [file]
"""Launch and stop the httpd test server.
Port of the lifecycle bits of Apache::TestServer. Builds the same command line
(``httpd -d <serverroot> -f <conf> -D APACHE2 -D APACHE2_4 <defines>``), starts
the server, waits for it to serve, and stops it cleanly.
The process handling -- stale servers cleared up front like Apache::TestServer's
``sub start`` does via ``sub stop``, its own session so the whole process group
can be signalled and no children are left holding ports after a failed or
interrupted run -- is :class:`pyhttpd.instance.HttpdInstance`, shared with the
modules/ suite. This class only knows the command line of the generated config.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from pyhttpd.instance import HttpdInstance, HttpdStartError
from .config import TestConfig
class HttpdServer:
def __init__(self, config: TestConfig) -> None:
self.config = config
self._instance: HttpdInstance | None = None
@property
def instance(self) -> HttpdInstance:
if self._instance is None:
vars_ = self.config.vars
self._instance = HttpdInstance(
self.config.info.httpd,
[
"-d",
vars_["serverroot"],
"-f",
vars_["t_conf_file"],
"-DFOREGROUND",
*[a for d in self.dversion_defines() for a in ("-D", d)],
],
ports=[int(vars_["port"])],
error_log=Path(vars_["t_logs"]) / "error_log",
pid_files=[Path(vars_["t_pid_file"])],
stderr_file=Path(vars_["t_logs"]) / "httpd.stderr",
# Like Apache::TestServer, validate the config before launch
configtest_first=True,
)
return self._instance
@property
def proc(self) -> subprocess.Popen[bytes] | None:
return self.instance.proc
# -- command line -----------------------------------------------------
def dversion_defines(self) -> list[str]:
"""Version defines for .conf conditionals (TestServer::dversion).
rev 2 always gets ``APACHE2``; the 2.4+ line keeps ``APACHE2_4`` (the
``<IfVersion>`` blocks handle finer-grained 2.5 gating). User ``-D``
defines are appended.
"""
v = self.config.info.version
defs = [f"APACHE{v[0]}"]
if v[0] == 2 and v[1] >= 4:
defs.append("APACHE2_4")
defs += self.config.defines
return defs
def args(self) -> list[str]:
return self.instance.argv()
# -- syntax check -----------------------------------------------------
def configtest(self) -> subprocess.CompletedProcess[str]:
"""Run ``httpd ... -t`` to validate the generated config before launch."""
return self.instance.configtest()
# -- lifecycle --------------------------------------------------------
@property
def port(self) -> int:
return int(self.config.vars["port"])
def start(self, *, timeout: float = 30.0) -> None:
try:
self.instance.start(timeout=timeout)
except HttpdStartError as exc:
raise RuntimeError(
f"{exc}; see {self.config.vars['t_logs']}/error_log\n{exc.stderr}"
) from exc
def stop(self, *, timeout: float = 10.0) -> None:
"""Stop the server, reaping the whole process group, idempotently.
Safe to call when the server was never started; also clears any
matching stale pid file.
"""
self.instance.stop(timeout=timeout)