This is an automated email from the ASF dual-hosted git repository.

dsoumis pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 509a8b42238ccfedfcf51570df2ac053447dd1f7
Author: Dimitris Soumis <[email protected]>
AuthorDate: Tue Feb 17 16:34:44 2026 +0200

    Add httpd integration testing infrastructure
---
 .../httpd/HttpdIntegrationBaseTest.java            | 154 +++++++++++++++++++++
 .../tomcat/integration/httpd/TesterHttpd.java      | 149 ++++++++++++++++++++
 .../tomcat/integration/httpd/httpd-binary.lock     |   0
 3 files changed, 303 insertions(+)

diff --git 
a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java 
b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java
new file mode 100644
index 0000000000..17930db98d
--- /dev/null
+++ b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tomcat.integration.httpd;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.ServerSocket;
+import java.nio.channels.FileLock;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Valve;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.compat.JrePlatform;
+
+/**
+ * Base class for httpd integration tests.
+ * Manages httpd and Tomcat process lifecycle.
+ */
+public abstract class HttpdIntegrationBaseTest extends TomcatBaseTest {
+
+    private static final File lockFile = new 
File("test/org/apache/tomcat/integration/httpd/httpd-binary.lock");
+    private static FileLock lock = null;
+
+    private TesterHttpd httpd;
+    private int httpdPort;
+    protected File httpdConfDir;
+
+    private int tomcatPort;
+
+    protected abstract String getHttpdConfig();
+    protected abstract List<Valve> getValveConfig();
+
+    @BeforeClass
+    public static void obtainHttpdBinaryLock() throws IOException {
+        @SuppressWarnings("resource")
+        FileOutputStream fos = new FileOutputStream(lockFile);
+        lock = fos.getChannel().lock();
+    }
+
+    @AfterClass
+    public static void releaseHttpdBinaryLock() throws IOException {
+        // Should not be null be in case obtaining the lock fails, avoid a 
second error.
+        if (lock != null) {
+            lock.release();
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        setUpTomcat();
+        setUpHttpd();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        if (httpd != null) {
+            httpd.stop();
+            httpd = null;
+        }
+        super.tearDown();
+    }
+
+    private void setUpTomcat() throws LifecycleException {
+        Tomcat tomcat = getTomcatInstance();
+        Context ctx = getProgrammaticRootContext();
+        for (Valve valve : getValveConfig()) {
+            ctx.getPipeline().addValve(valve);
+        }
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/snoop", "snoop");
+        tomcat.start();
+        tomcatPort = getPort();
+    }
+
+    private void setUpHttpd() throws IOException {
+        httpdPort = findFreePort();
+        httpdConfDir = getTemporaryDirectory();
+        generateHttpdConfig(getHttpdConfig());
+
+        httpd = new TesterHttpd(httpdConfDir, httpdPort);
+        try {
+            httpd.start();
+        } catch (IOException | InterruptedException ioe) {
+            httpd = null;
+        }
+    }
+
+    private static int findFreePort() throws IOException {
+        try (ServerSocket socket = new ServerSocket(0)) {
+            return socket.getLocalPort();
+        }
+    }
+
+    public void generateHttpdConfig(String httpdConf) throws IOException {
+
+        httpdConf = getPlatformHttpdConfig() + httpdConf;
+
+        httpdConf = httpdConf.replace("%{HTTPD_PORT}", 
Integer.toString(httpdPort))
+                             .replace("%{TOMCAT_PORT}", 
Integer.toString(tomcatPort))
+                             .replace("%{CONF_DIR}", 
httpdConfDir.getAbsolutePath());
+
+
+        try (PrintWriter writer = new PrintWriter(new File(httpdConfDir, 
"httpd.conf"))) {
+            writer.write(httpdConf);
+        }
+
+    }
+
+    private String getPlatformHttpdConfig() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Listen %{HTTPD_PORT}\n");
+        sb.append("PidFile %{CONF_DIR}/httpd.pid\n");
+        sb.append("LoadModule authz_core_module modules/mod_authz_core.so\n");
+        if (JrePlatform.IS_WINDOWS) {
+            sb.append("LoadModule mpm_winnt_module 
modules/mod_mpm_winnt.so\n");
+            sb.append("ErrorLog \"|C:/Windows/System32/more.com\"\n");
+        } else {
+            sb.append("LoadModule unixd_module modules/mod_unixd.so\n");
+            sb.append("LoadModule mpm_event_module 
modules/mod_mpm_event.so\n");
+            sb.append("ErrorLog /dev/stderr\n");
+        }
+        sb.append("LogLevel warn\n");
+        sb.append("ServerName localhost:%{HTTPD_PORT}\n");
+        return sb.toString();
+    }
+
+    public int getHttpdPort() {
+        return httpdPort;
+    }
+}
diff --git a/test/org/apache/tomcat/integration/httpd/TesterHttpd.java 
b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java
new file mode 100644
index 0000000000..7502af76ad
--- /dev/null
+++ b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tomcat.integration.httpd;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Assert;
+
+
+public class TesterHttpd {
+
+    private final File httpdConfDir;
+    private final int httpdPort;
+
+    private static final String HTTPD_PATH = "tomcat.test.httpd.path";
+
+    private Process p;
+
+    public TesterHttpd(File httpdConfDir, int httpdPort) {
+        this.httpdConfDir = httpdConfDir;
+        this.httpdPort = httpdPort;
+    }
+
+    public void start() throws IOException, InterruptedException {
+        start(false);
+    }
+
+    public void start(boolean swallowOutput) throws IOException, 
InterruptedException {
+        if (p != null) {
+            throw new IllegalStateException("Already started");
+        }
+
+        String httpdPath = System.getProperty(HTTPD_PATH);
+        if (httpdPath == null) {
+            httpdPath = "httpd";
+        }
+
+        File httpdConfFile = new File(httpdConfDir, "httpd.conf");
+        validateHttpdConfig(httpdPath, httpdConfFile.getAbsolutePath());
+
+        List<String> cmd = new ArrayList<>(4);
+        cmd.add(httpdPath);
+        cmd.add("-f");
+        cmd.add(httpdConfFile.getAbsolutePath());
+        cmd.add("-X");
+
+        ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0]));
+
+        p = pb.start();
+
+        redirect(p.inputReader(), System.out, swallowOutput);
+        redirect(p.errorReader(), System.err, swallowOutput);
+
+        Assert.assertTrue(p.isAlive() && isHttpdReady());
+    }
+
+    public void stop() {
+        if (p == null) {
+            throw new IllegalStateException("Not started");
+        }
+        p.destroy();
+
+        try {
+            if (!p.waitFor(30, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Failed to stop");
+            }
+        } catch (InterruptedException e) {
+            throw new IllegalStateException("Interrupted while waiting to 
stop", e);
+        }
+    }
+
+    private void redirect(final Reader r, final PrintStream os, final boolean 
swallow) {
+        /*
+         * InputStream will close when process ends. Thread will exit once 
stream closes.
+         */
+        new Thread( () -> {
+            char[] cbuf = new char[1024];
+            try {
+                int read;
+                while ((read = r.read(cbuf)) > 0) {
+                    if (!swallow) {
+                        os.print(new String(cbuf, 0, read));
+                    }
+                }
+            } catch (IOException ignore) {
+                // Ignore
+            }
+
+        }).start();
+    }
+
+    private static void validateHttpdConfig(final String httpdPath, final 
String httpdConfPath) throws IOException, InterruptedException {
+        List<String> cmd = new ArrayList<>(4);
+
+        cmd.add(httpdPath);
+        cmd.add("-t");
+        cmd.add("-f");
+        cmd.add(httpdConfPath);
+
+        ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0]));
+        pb.redirectErrorStream(true);
+
+        Process p = pb.start();
+
+        String output = new String(p.getInputStream().readAllBytes());
+        int exitCode = p.waitFor();
+
+        if (exitCode != 0) {
+            throw new IllegalStateException("Httpd configuration invalid. 
Output: " + output);
+        }
+    }
+
+    @SuppressWarnings("BusyWait")
+    private boolean isHttpdReady() throws InterruptedException {
+        long deadline = System.currentTimeMillis() + 1000;
+        while (System.currentTimeMillis() < deadline) {
+            try (Socket ignored = new Socket("localhost", this.httpdPort)) {
+                return true;
+            } catch (IOException e) {
+                Thread.sleep(100);
+            }
+        }
+        throw new IllegalStateException("Httpd has not been started.");
+    }
+
+
+}
diff --git a/test/org/apache/tomcat/integration/httpd/httpd-binary.lock 
b/test/org/apache/tomcat/integration/httpd/httpd-binary.lock
new file mode 100644
index 0000000000..e69de29bb2


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to