Skip to content

Commit a905dff

Browse files
committed
Adds a custom pytest option for archiving logs and conf files across different test executions
1 parent 51912a2 commit a905dff

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

test/README.pytest

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,58 @@ also raises the error log level of the tested modules.
3535
> pytest -vvv -k test_h2_004_01
3636
run the specific test with mod_http2 at log level TRACE2.
3737

38+
There is an option to archive the results across different
39+
modules and httpd versions. The archiving will preserve
40+
error_log and access_log (cumulative for all tests in the module),
41+
and save a separate config file for each test case.
42+
> pytest -k test_core --archive=/path/to/archive/
43+
44+
If you don't provide any specific module, pytest will execute all of them
45+
and the already archived folders and files will be replaced by the new ones.
46+
> pytest --archive=/path/to/archive
47+
48+
Always use --archive=<path> (with =, not a space) to avoid pytest rootdir issues.
49+
50+
Using the --archive option having installed a different httpd version
51+
will preserve any results from previous executions so you would
52+
end up with a different folder structure for each httpd version.
53+
You can also archive the results of different modules and that will
54+
work incrementally, which means it will add a new folder per module to the structure.
55+
56+
57+
What gets archived:
58+
- error_log and access_log for the module run
59+
- per-test configs: test.conf after each test runs
60+
- module configs: modules.conf, stop.conf
61+
- full server directory (conf, logs, htdocs)
62+
- shared infrastructure (CA, mod_md store, pebble files) in _shared/ to avoid duplication
63+
64+
Example archive structure with 2 httpd versions:
65+
├── /path/to/archive
66+
│ ├── 2.4.65
67+
│ │ ├── _shared
68+
│ │ │ ├── conf
69+
│ │ │ │ ├── httpd.conf
70+
│ │ │ │ └── mime.types
71+
│ │ │ ├── ca
72+
│ │ │ ├── md
73+
│ │ │ ├── acme-ca.pem
74+
│ │ │ └── eab.json
75+
│ │ ├── core
76+
│ │ │ ├── conf
77+
│ │ │ │ ├── modules.conf
78+
│ │ │ │ ├── test_core_001_01.conf
79+
│ │ │ │ ├── test_core_002_01.conf
80+
│ │ │ │ └── ...
81+
│ │ │ ├── logs
82+
│ │ │ │ ├── error_log
83+
│ │ │ │ └── access_log
84+
│ │ │ └── htdocs
85+
│ │ ├── http1
86+
│ │ └── http2
87+
│ └── 2.4.66
88+
│ └── core
89+
3890
By default, test cases will configure httpd with mpm_event. You
3991
can change that with the invocation:
4092
> MPM=worker pytest test/modules/http2

test/conftest.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import os
3+
import warnings
34

45
import pytest
56

@@ -15,6 +16,8 @@ def pytest_addoption(parser):
1516
parser.addoption("--repeat", action="store", type=int, default=1,
1617
help='Number of times to repeat each test')
1718
parser.addoption("--all", action="store_true")
19+
parser.addoption("--archive", action="store", default=None,
20+
help='Archive the server directory after each test package to the specified folder')
1821

1922

2023
def pytest_generate_tests(metafunc):
@@ -29,6 +32,12 @@ def _function_scope(env, request):
2932
env.set_current_test_name(request.node.name)
3033
yield
3134
env.check_error_log()
35+
archive_dir = request.config.getoption("--archive")
36+
if archive_dir:
37+
fspath = str(request.fspath)
38+
if 'modules/' in fspath:
39+
package_name = fspath.split('modules/')[1].split('/')[0]
40+
env.archive_test_conf(request.node.name, package_name, archive_dir)
3241
env.set_current_test_name(None)
3342

3443

@@ -39,9 +48,18 @@ def _module_scope(env):
3948

4049

4150
@pytest.fixture(autouse=True, scope="package")
42-
def _package_scope(env):
51+
def _package_scope(env, request):
4352
env.httpd_error_log.clear_ignored_matches()
4453
env.httpd_error_log.clear_ignored_lognos()
4554
yield
4655
assert env.apache_stop() == 0
4756
env.check_error_log()
57+
58+
archive_dir = request.config.getoption("--archive")
59+
if archive_dir == "":
60+
warnings.warn("--archive option was empty, skipping archiving")
61+
if archive_dir:
62+
fspath = str(request.fspath)
63+
parts = fspath.split('modules/')
64+
package_name = parts[1].split('/')[0]
65+
env.archive_logs(package_name, archive_dir)

test/pyhttpd/env.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import glob
12
import importlib
23
import inspect
34
import logging
@@ -20,7 +21,6 @@
2021
from .nghttp import Nghttp
2122
from .result import ExecResult
2223

23-
2424
log = logging.getLogger(__name__)
2525

2626

@@ -29,7 +29,6 @@ class Dummy:
2929

3030

3131
class HttpdTestSetup:
32-
3332
# the modules we want to load
3433
MODULES = [
3534
"log_config",
@@ -209,8 +208,8 @@ def _build_clients(self):
209208

210209

211210
class HttpdTestEnv:
212-
213211
LIBEXEC_DIR = None
212+
SHARED_SERVER_ENTRIES = {'ca', 'md', 'acme-ca.pem', 'eab.json'}
214213

215214
@classmethod
216215
def has_python_package(cls, name: str) -> bool:
@@ -337,9 +336,60 @@ def setup_httpd(self, setup: HttpdTestSetup = None):
337336

338337
def check_error_log(self):
339338
errors, warnings = self._error_log.get_missed()
340-
assert (len(errors), len(warnings)) == (0, 0),\
341-
f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n"\
342-
"{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings))
339+
assert (len(errors), len(warnings)) == (0, 0), \
340+
f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n" \
341+
"{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings))
342+
343+
# archives preserving metadata and avoiding duplication
344+
def archive_logs(self, package_name, archive_dir):
345+
version = self.get_httpd_version()
346+
dest = os.path.join(archive_dir, version, package_name)
347+
if os.path.isdir(dest):
348+
shutil.rmtree(dest)
349+
350+
# use ignore argument to avoid duplication of files
351+
shutil.copytree(self._server_dir, dest, ignore=self.ignore_files)
352+
shared_dest = os.path.join(archive_dir, version, 'shared')
353+
354+
if os.path.isdir(shared_dest):
355+
shutil.rmtree(shared_dest)
356+
os.makedirs(shared_dest)
357+
358+
for entry in self.SHARED_SERVER_ENTRIES:
359+
src = os.path.join(self._server_dir, entry)
360+
entry_dest = os.path.join(shared_dest, entry)
361+
if os.path.isdir(src):
362+
shutil.copytree(src, entry_dest)
363+
elif os.path.isfile(src):
364+
shutil.copy2(src, entry_dest)
365+
366+
shared_conf_dest = os.path.join(shared_dest, 'conf')
367+
os.makedirs(shared_conf_dest)
368+
369+
# copy them once
370+
for f in ['httpd.conf', 'mime.types']:
371+
src = os.path.join(self._server_conf_dir, f)
372+
if os.path.isfile(src):
373+
shutil.copy2(src, shared_conf_dest)
374+
375+
def archive_test_conf(self, test_name, package_name, archive_dir):
376+
version = self.get_httpd_version()
377+
dest = os.path.join(archive_dir, version, package_name, 'conf')
378+
if not os.path.isdir(dest):
379+
os.makedirs(dest)
380+
381+
test_conf = os.path.join(self._server_conf_dir, 'test.conf')
382+
if os.path.isfile(test_conf):
383+
final_name = test_name.replace('/', '_').replace('\\', '_')
384+
dest_file = os.path.join(dest, f"{final_name}.conf")
385+
shutil.copy(test_conf, dest_file)
386+
387+
# return files to ignore
388+
def ignore_files(self, d, entries):
389+
ignored = [e for e in entries if e.endswith('.sock') or e in self.SHARED_SERVER_ENTRIES]
390+
if os.path.basename(d) == 'conf':
391+
ignored += ['httpd.conf', 'mime.types']
392+
return ignored
343393

344394
@property
345395
def curl(self) -> str:
@@ -687,7 +737,7 @@ def apache_restart(self):
687737
timeout = timedelta(seconds=10)
688738
return 0 if self.is_live(self._http_base, timeout=timeout) else -1
689739
return r.exit_code
690-
740+
691741
def apache_stop(self):
692742
r = self._run_apachectl("stop")
693743
if r.exit_code == 0:
@@ -776,6 +826,7 @@ def curl_parse_headerfile(self, headerfile: str, r: ExecResult = None) -> ExecRe
776826
r = ExecResult(args=[], exit_code=0, stdout=b'', stderr=b'')
777827

778828
response = None
829+
779830
def fin_response(response):
780831
if response:
781832
r.add_response(response)
@@ -876,7 +927,7 @@ def curl_protocol_version(self, url, timeout=5, options=None):
876927
if r.exit_code == 0 and r.response:
877928
return r.response["body"].decode('utf-8').rstrip()
878929
return -1
879-
930+
880931
def nghttp(self):
881932
return Nghttp(self._nghttp, connect_addr=self._httpd_addr,
882933
tmp_dir=self.gen_dir, test_name=self._current_test)
@@ -918,4 +969,3 @@ def make_data_file(self, indir: str, fname: str, fsize: int) -> str:
918969
s = f"{i:09d}-{s}\n"
919970
fd.write(s[0:remain])
920971
return fpath
921-

0 commit comments

Comments
 (0)