This is an automated email from the ASF dual-hosted git repository. dsoumis pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a350d032be4c15ebeee20599b0e9eb10a5094cec 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]
