Skip to content

Commit 7ed0cab

Browse files
committed
Adds a custom pytest option for archiving logs and conf files across different test executions
1 parent 08d2553 commit 7ed0cab

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:
@@ -339,9 +338,60 @@ def setup_httpd(self, setup: HttpdTestSetup = None):
339338

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

346396
@property
347397
def curl(self) -> str:
@@ -689,7 +739,7 @@ def apache_restart(self):
689739
timeout = timedelta(seconds=10)
690740
return 0 if self.is_live(self._http_base, timeout=timeout) else -1
691741
return r.exit_code
692-
742+
693743
def apache_stop(self):
694744
r = self._run_apachectl("stop")
695745
if r.exit_code == 0:
@@ -778,6 +828,7 @@ def curl_parse_headerfile(self, headerfile: str, r: ExecResult = None) -> ExecRe
778828
r = ExecResult(args=[], exit_code=0, stdout=b'', stderr=b'')
779829

780830
response = None
831+
781832
def fin_response(response):
782833
if response:
783834
r.add_response(response)
@@ -878,7 +929,7 @@ def curl_protocol_version(self, url, timeout=5, options=None):
878929
if r.exit_code == 0 and r.response:
879930
return r.response["body"].decode('utf-8').rstrip()
880931
return -1
881-
932+
882933
def nghttp(self):
883934
return Nghttp(self._nghttp, connect_addr=self._httpd_addr,
884935
tmp_dir=self.gen_dir, test_name=self._current_test)
@@ -920,4 +971,3 @@ def make_data_file(self, indir: str, fname: str, fsize: int) -> str:
920971
s = f"{i:09d}-{s}\n"
921972
fd.write(s[0:remain])
922973
return fpath
923-

0 commit comments

Comments
 (0)