]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Add --archive option to preserve test results trunk trunk
authorGiannis Christodoulou <ichristod@apache.org>
Fri, 7 Aug 2026 16:02:07 +0000 (16:02 +0000)
committerGiannis Christodoulou <ichristod@apache.org>
Fri, 7 Aug 2026 16:02:07 +0000 (16:02 +0000)
  * test/conftest.py: Add --archive command-line option. Save per-test
    conf snapshots via archive_test_conf after each test. Call archive_logs
    at end of each package.

  * test/pyhttpd/env.py: Add archive_logs and archive_test_conf to copy
    logs and per-test confs to the archive folder, organised by httpd
    version and git/svn revision. Add ignore_files to exclude htdocs,
    shared confs and sockets from per-module copies. Move shared entries
    (ca, ca1, ca2, certs, httpd.conf, mime.types, stop.conf) to _shared/.
    Wipe logs dir at module setup to avoid stale files from previous runs.

  * test/README.pytest: Document the --archive option, folder structure
    and what gets archived.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1936981 13f79535-47bb-0310-9956-ffa450edef68

test/README.pytest
test/conftest.py
test/pyhttpd/env.py

index e6630ac394fe2c1534f0be980526649875f7b8b5..0b5cfd57a4aebd5c9ccbd28842ab051df2426933 100644 (file)
@@ -35,6 +35,50 @@ also raises the error log level of the tested modules.
 > pytest -vvv -k test_h2_004_01
 run the specific test with mod_http2 at log level TRACE2.
 
+There is an option to archive the results across different
+modules and httpd versions. The archiving will preserve
+error_log and access_log (cumulative for all tests in the module),
+and save a separate config file for each test case.
+> pytest test/modules/core --archive=/path/to/archive
+
+If you don't provide any specific module, pytest will execute all of them
+and add a new folder per module to the existing structure.
+> pytest --archive=/path/to/archive
+
+Always use --archive=<path> (with =, not a space) to avoid pytest rootdir issues.
+
+The archive folder is named after the httpd version and the git commit hash
+so results from different code versions are kept separate.
+Running the same module again with the same commit updates its folder in place.
+
+What gets archived:
+- error_log and access_log for the module run
+- per-test configs: test.conf snapshot after each test
+- module config: modules.conf
+- shared configs and infrastructure in _shared/ to avoid duplication
+
+Example archive structure with 2 httpd versions:
+    ├── /path/to/archive
+    │   ├── 2.4.65-a3f8c1d
+    │   │   ├── _shared
+    │   │   │   ├── conf
+    │   │   │   │   ├── httpd.conf
+    │   │   │   │   ├── mime.types
+    │   │   │   │   └── stop.conf
+    │   │   │   ├── ca
+    │   │   ├── core
+    │   │   │   ├── conf
+    │   │   │   │   ├── modules.conf
+    │   │   │   │   ├── test_core_001_01.conf
+    │   │   │   └── logs
+    │   │   │       ├── error_log
+    │   │   │       └── access_log
+    │   │   ├── http1
+    │   │   └── http2
+    │   └── 2.4.66-b7d2e4f
+    │        ├── _shared
+    │        └── core
+
 By default, test cases will configure httpd with mpm_event. You
 can change that with the invocation:
 > MPM=worker pytest test/modules/http2
index 390a33bec5d39a68c397f2280974999a0b3e99ad..1fa0e751e08eee0b94fa653b1591eb6dde1f2b73 100644 (file)
@@ -1,5 +1,6 @@
 import sys
 import os
+import warnings
 
 import pytest
 
@@ -15,6 +16,9 @@ def pytest_addoption(parser):
     parser.addoption("--repeat", action="store", type=int, default=1,
                      help='Number of times to repeat each test')
     parser.addoption("--all", action="store_true")
+    parser.addoption("--archive", action="store", default=None,
+                     help='Archive the server dir after each test package '
+                          'to the specified folder')
 
 
 def pytest_generate_tests(metafunc):
@@ -29,6 +33,12 @@ def _function_scope(env, request):
     env.set_current_test_name(request.node.name)
     yield
     env.check_error_log()
+    archive_dir = request.config.getoption("--archive")
+    if archive_dir:
+        fspath = str(request.fspath)
+        if 'modules/' in fspath:
+            package_name = fspath.split('modules/')[1].split('/')[0]
+            env.archive_test_conf(request.node.name, package_name, archive_dir)
     env.set_current_test_name(None)
 
 
@@ -39,9 +49,17 @@ def _module_scope(env):
 
 
 @pytest.fixture(autouse=True, scope="package")
-def _package_scope(env):
+def _package_scope(env, request):
     env.httpd_error_log.clear_ignored_matches()
     env.httpd_error_log.clear_ignored_lognos()
     yield
     assert env.apache_stop() == 0
     env.check_error_log()
+    archive_dir = request.config.getoption("--archive")
+    if archive_dir == "":
+        warnings.warn("--archive option was empty, skipping archiving")
+    if archive_dir:
+        fspath = str(request.fspath)
+        parts = fspath.split('modules/')
+        package_name = parts[1].split('/')[0]
+        env.archive_logs(package_name, archive_dir)
index a3e020dd99475d3ff98c0e2579923fab37f14ea8..0ecc31b96dc20d995af11af21720b4215571027e 100644 (file)
@@ -99,8 +99,10 @@ class HttpdTestSetup:
     def _make_dirs(self):
         if not os.path.exists(self.env.gen_dir):
             os.makedirs(self.env.gen_dir)
-        if not os.path.exists(self.env.server_logs_dir):
-            os.makedirs(self.env.server_logs_dir)
+        # wipe logs from any previous module run to avoid stale files in the archive
+        if os.path.isdir(self.env.server_logs_dir):
+            shutil.rmtree(self.env.server_logs_dir)
+        os.makedirs(self.env.server_logs_dir)
 
     def _make_conf(self):
         # remove anything from another run/test suite
@@ -224,6 +226,9 @@ class HttpdTestSetup:
 class HttpdTestEnv:
 
     LIBEXEC_DIR = None
+    # entries shared across all modules, archived once in _shared
+    SHARED_SERVER_ENTRIES = {'ca', 'ca1', 'ca2', 'md', 'acme-ca.pem',
+                             'eab.json'}
 
     @staticmethod
     def has_tool(name: str) -> bool:
@@ -360,6 +365,81 @@ class HttpdTestEnv:
                 f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n"\
                 "{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings))
 
+    # combines httpd version & revision to identify the archive folder
+    def _archive_version(self):
+        if not hasattr(self, '_cached_archive_version'):
+            version = self.get_httpd_version()
+            r = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
+                               capture_output=True, text=True,
+                               cwd=self._our_dir)
+            if r.returncode == 0:
+                version = f"{version}-{r.stdout.strip()}"
+            else:
+                r = subprocess.run(['svn', 'info', '--show-item', 'revision'],
+                                   capture_output=True, text=True,
+                                   cwd=self._our_dir)
+
+                if r.returncode == 0:
+                    version = f"{version}-r{r.stdout.strip()}"
+            self._cached_archive_version = version
+        return self._cached_archive_version
+
+    def archive_logs(self, package_name, archive_dir):
+        version = self._archive_version()
+        dest = os.path.join(archive_dir, version, package_name)
+        os.makedirs(dest, exist_ok=True)
+
+        # merge into existing dest to preserve per-test confs
+        shutil.copytree(self._server_dir, dest, ignore=self.ignore_files,
+                        dirs_exist_ok=True)
+
+        shared_dest = os.path.join(archive_dir, version, '_shared')
+
+        if os.path.isdir(shared_dest):
+            shutil.rmtree(shared_dest)
+        os.makedirs(shared_dest)
+
+        for entry in self.SHARED_SERVER_ENTRIES:
+            src = os.path.join(self._server_dir, entry)
+            entry_dest = os.path.join(shared_dest, entry)
+            if os.path.isdir(src):
+                shutil.copytree(src, entry_dest)
+            elif os.path.isfile(src):
+                shutil.copy2(src, entry_dest)
+
+        shared_conf_dest = os.path.join(shared_dest, 'conf')
+        os.makedirs(shared_conf_dest)
+
+        # copy them once
+        for f in ['httpd.conf', 'mime.types', 'stop.conf']:
+            src = os.path.join(self._server_conf_dir, f)
+            if os.path.isfile(src):
+                shutil.copy2(src, shared_conf_dest)
+
+    def archive_test_conf(self, test_name, package_name, archive_dir):
+        version = self._archive_version()
+        dest = os.path.join(archive_dir, version, package_name, 'conf')
+        if not os.path.isdir(dest):
+            os.makedirs(dest)
+        test_conf = os.path.join(self._server_conf_dir, 'test.conf')
+        if os.path.isfile(test_conf):
+            safe_name = test_name.replace('/', '_').replace('\\', '_')
+            dest_file = os.path.join(dest, f"{safe_name}.conf")
+            shutil.copy(test_conf, dest_file)
+
+    # return files to ignore
+    def ignore_files(self, d, entries):
+        ignored = [e for e in entries if
+                   e.endswith('.sock') or e in self.SHARED_SERVER_ENTRIES]
+        # shared confs goes to _shared
+        # test.conf is captured per-test
+        if os.path.basename(d) == 'conf':
+            ignored += ['httpd.conf', 'mime.types', 'stop.conf', 'test.conf']
+        # htdocs are static test fixtures not useful in the archive
+        if os.path.basename(d) == os.path.basename(self._server_dir):
+            ignored += ['htdocs']
+        return ignored
+
     @property
     def curl(self) -> str:
         return self._curl