Jelajahi Sumber

Parallel test on CI (#2364)

* Parallel test on CI

* Fix problem with Windows

* Use cache for vcpkg

* Parallel 'No Exception' test

* Use one job to run all shards
yhirose 1 bulan lalu
induk
melakukan
ed5c5d325b

+ 39 - 5
.github/workflows/test.yaml

@@ -94,12 +94,12 @@ jobs:
         run: sudo apt-get install -y libmbedtls-dev
       - name: build and run tests (OpenSSL)
         if: matrix.tls_backend == 'openssl'
-        run: cd test && make
+        run: cd test && make test_split && make test_openssl_parallel
         env:
           LSAN_OPTIONS: suppressions=lsan_suppressions.txt
       - name: build and run tests (Mbed TLS)
         if: matrix.tls_backend == 'mbedtls'
-        run: cd test && make test_split_mbedtls && make test_mbedtls && ./test_mbedtls
+        run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
       - name: run fuzz test target
         if: matrix.tls_backend == 'openssl'
         run: cd test && make fuzz_test
@@ -123,12 +123,12 @@ jobs:
         run: brew install mbedtls@3
       - name: build and run tests (OpenSSL)
         if: matrix.tls_backend == 'openssl'
-        run: cd test && make
+        run: cd test && make test_split && make test_openssl_parallel
         env:
           LSAN_OPTIONS: suppressions=lsan_suppressions.txt
       - name: build and run tests (Mbed TLS)
         if: matrix.tls_backend == 'mbedtls'
-        run: cd test && make test_split_mbedtls && make test_mbedtls && ./test_mbedtls
+        run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
       - name: run fuzz test target
         if: matrix.tls_backend == 'openssl'
         run: cd test && make fuzz_test
@@ -171,7 +171,14 @@ jobs:
           core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
     - name: Setup msbuild on windows
       uses: microsoft/setup-msbuild@v2
+    - name: Cache vcpkg packages
+      id: vcpkg-cache
+      uses: actions/cache@v4
+      with:
+        path: C:/vcpkg/installed
+        key: vcpkg-installed-windows-gtest-curl-zlib-brotli-zstd
     - name: Install vcpkg dependencies
+      if: steps.vcpkg-cache.outputs.cache-hit != 'true'
       run: vcpkg install gtest curl zlib brotli zstd
     - name: Install OpenSSL
       if: ${{ matrix.config.with_ssl }}
@@ -192,7 +199,34 @@ jobs:
       run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
     - name: Run tests ${{ matrix.config.name }}
       if: ${{ matrix.config.run_tests }}
-      run: ctest --output-on-failure --test-dir build -C Release -E "BenchmarkTest"
+      shell: pwsh
+      working-directory: build/test
+      run: |
+        $shards = 4
+        $procs = @()
+        for ($i = 0; $i -lt $shards; $i++) {
+          $log = "shard_${i}.log"
+          $procs += Start-Process -FilePath ./Release/httplib-test.exe `
+            -ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}-*BenchmarkTest*" `
+            -NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
+            -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
+        }
+        $procs | Wait-Process
+        $failed = $false
+        for ($i = 0; $i -lt $shards; $i++) {
+          $log = "shard_${i}.log"
+          if (Select-String -Path $log -Pattern "\[  PASSED  \]" -Quiet) {
+            $passed = (Select-String -Path $log -Pattern "\[  PASSED  \]").Line
+            Write-Host "Shard ${i}: $passed"
+          } else {
+            Write-Host "=== Shard $i FAILED ==="
+            Get-Content $log
+            if (Test-Path "${log}.err") { Get-Content "${log}.err" }
+            $failed = $true
+          }
+        }
+        if ($failed) { exit 1 }
+        Write-Host "All shards passed."
     - name: Run benchmark tests with retry ${{ matrix.config.name }}
       if: ${{ matrix.config.run_tests }}
       run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5

+ 1 - 1
.github/workflows/test_no_exceptions.yaml

@@ -17,4 +17,4 @@ jobs:
 
     - name: Run tests with CPPHTTPLIB_NO_EXCEPTIONS
       run: |
-        cd test && make EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS"
+        cd test && make test_split EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS" && make test_openssl_parallel EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS"

+ 2 - 2
.github/workflows/test_offline.yaml

@@ -49,12 +49,12 @@ jobs:
           sudo ip6tables -A OUTPUT -j REJECT
       - name: build and run tests (OpenSSL)
         if: matrix.tls_backend == 'openssl'
-        run: cd test && make
+        run: cd test && make test_split && make test_openssl_parallel
         env:
           LSAN_OPTIONS: suppressions=lsan_suppressions.txt
       - name: build and run tests (No TLS)
         if: matrix.tls_backend == 'no-tls'
-        run: cd test && make test_no_tls && ./test_no_tls
+        run: cd test && make test_no_tls_parallel
       - name: restore network
         if: always()
         run: |

+ 1 - 0
.gitignore

@@ -46,6 +46,7 @@ test/test.xcodeproj/*/xcuser*
 test/*.o
 test/*.pem
 test/*.srl
+test/*.log
 test/_build_*
 work/
 benchmark/server*

+ 9 - 0
justfile

@@ -9,13 +9,22 @@ openssl:
     @(cd test && make test && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test)
     @(cd test && make proxy)
 
+openssl_parallel:
+    @(cd test && make test_openssl_parallel)
+
 mbedtls:
     @(cd test && make test_mbedtls && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test_mbedtls)
     @(cd test && make proxy_mbedtls)
 
+mbedtls_parallel:
+    @(cd test && make test_mbedtls_parallel)
+
 no_tls:
     @(cd test && make test_no_tls && ./test_no_tls)
 
+no_tls_parallel:
+    @(cd test && make test_no_tls_parallel)
+
 fuzz:
     @(cd test && make fuzz_test)
 

+ 37 - 1
test/Makefile

@@ -56,6 +56,42 @@ STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
 all : test test_split
 	LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test
 
+SHARDS ?= 4
+
+define run_parallel
+	@echo "Running $(1) with $(SHARDS) shards in parallel..."
+	@fail=0; \
+	for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
+	  GTEST_TOTAL_SHARDS=$(SHARDS) GTEST_SHARD_INDEX=$$i \
+	  LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
+	  ./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
+	done; \
+	wait; \
+	for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
+	  if ! grep -q "\[  PASSED  \]" $(1)_shard_$$i.log; then \
+	    echo "=== Shard $$i FAILED ==="; \
+	    cat $(1)_shard_$$i.log; \
+	    fail=1; \
+	  else \
+	    passed=$$(grep "\[  PASSED  \]" $(1)_shard_$$i.log); \
+	    echo "Shard $$i: $$passed"; \
+	  fi; \
+	done; \
+	if [ $$fail -ne 0 ]; then exit 1; fi; \
+	echo "All shards passed."
+endef
+
+.PHONY: test_openssl_parallel test_mbedtls_parallel test_no_tls_parallel
+
+test_openssl_parallel : test
+	$(call run_parallel,test)
+
+test_mbedtls_parallel : test_mbedtls
+	$(call run_parallel,test_mbedtls)
+
+test_no_tls_parallel : test_no_tls
+	$(call run_parallel,test_no_tls)
+
 proxy : test_proxy
 	@echo "Starting proxy server..."
 	cd proxy && \
@@ -172,5 +208,5 @@ cert.pem:
 	./gen-certs.sh
 
 clean:
-	rm -rf test test_split test_mbedtls test_split_mbedtls test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM
+	rm -rf test test_split test_mbedtls test_split_mbedtls test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log
 

+ 77 - 42
test/test.cc

@@ -55,7 +55,13 @@ using namespace std;
 using namespace httplib;
 
 const char *HOST = "localhost";
-const int PORT = 1234;
+
+static int get_base_port() {
+  const char *shard = getenv("GTEST_SHARD_INDEX");
+  return shard ? 11234 + std::atoi(shard) * 100 : 1234;
+}
+
+const int PORT = get_base_port();
 
 const string LONG_QUERY_VALUE = string(25000, '@');
 const string LONG_QUERY_URL = "/long-query-value?key=" + LONG_QUERY_VALUE;
@@ -178,7 +184,12 @@ protected:
     EXPECT_EQ(resp.body, content_);
   }
 
-  const std::string pathname_{"./httplib-server.sock"};
+  static std::string make_sock_path() {
+    const char *shard = getenv("GTEST_SHARD_INDEX");
+    return shard ? std::string("./httplib-server-") + shard + ".sock"
+                 : "./httplib-server.sock";
+  }
+  const std::string pathname_{make_sock_path()};
   const std::string pattern_{"/hi"};
   const std::string content_{"Hello World!"};
 };
@@ -2370,28 +2381,32 @@ TEST(RedirectFromPageWithContent, Redirect) {
 TEST(RedirectFromPageWithContentIP6, Redirect) {
   Server svr;
 
+  auto port_str = std::to_string(PORT);
+  auto redirect_url = "http://[::1]:" + port_str + "/2";
+  auto expected_host = "[::1]:" + port_str;
+
   svr.Get("/1", [&](const Request & /*req*/, Response &res) {
     res.set_content("___", "text/plain");
     // res.set_redirect("/2");
-    res.set_redirect("http://[::1]:1234/2");
+    res.set_redirect(redirect_url);
   });
 
   svr.Get("/2", [&](const Request &req, Response &res) {
     auto host_header = req.headers.find("Host");
     ASSERT_TRUE(host_header != req.headers.end());
-    EXPECT_EQ("[::1]:1234", host_header->second);
+    EXPECT_EQ(expected_host, host_header->second);
 
     res.set_content("Hello World!", "text/plain");
   });
 
-  auto th = std::thread([&]() { svr.listen("::1", 1234); });
+  auto th = std::thread([&]() { svr.listen("::1", PORT); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     th.join();
     ASSERT_FALSE(svr.is_running());
   });
 
-  // When IPV6 support isn't available svr.listen("::1", 1234) never
+  // When IPV6 support isn't available svr.listen("::1", PORT) never
   // actually starts anything, so the condition !svr.is_running() will
   // always remain true, and the loop never stops.
   // This basically counts how many milliseconds have passed since the
@@ -2403,7 +2418,7 @@ TEST(RedirectFromPageWithContentIP6, Redirect) {
   }
 
   {
-    Client cli("http://[::1]:1234");
+    Client cli("::1", PORT);
     cli.set_follow_location(true);
 
     std::string body;
@@ -2418,7 +2433,7 @@ TEST(RedirectFromPageWithContentIP6, Redirect) {
   }
 
   {
-    Client cli("http://[::1]:1234");
+    Client cli("::1", PORT);
 
     std::string body;
     auto res = cli.Get("/1", [&](const char *data, size_t data_length) {
@@ -10471,7 +10486,8 @@ TEST(ClientImplMethods, GetSocketTest) {
     res.status = StatusCode::OK_200;
   });
 
-  auto thread = std::thread([&]() { svr.listen("127.0.0.1", 3333); });
+  auto port = svr.bind_to_any_port("127.0.0.1");
+  auto thread = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     thread.join();
@@ -10481,7 +10497,7 @@ TEST(ClientImplMethods, GetSocketTest) {
   svr.wait_until_ready();
 
   {
-    httplib::Client cli("http://127.0.0.1:3333");
+    httplib::Client cli("127.0.0.1", port);
     cli.set_keep_alive(true);
 
     // Use the behavior of cpp-httplib of opening the connection
@@ -10640,10 +10656,13 @@ TEST(HttpsToHttpRedirectTest3, SimpleInterface_Online) {
 }
 
 TEST(HttpToHttpsRedirectTest, CertFile) {
+  auto ssl_port = PORT + 1;
+
   Server svr;
   ASSERT_TRUE(svr.is_valid());
   svr.Get("/index", [&](const Request &, Response &res) {
-    res.set_redirect("https://127.0.0.1:1235/index");
+    res.set_redirect("https://127.0.0.1:" + std::to_string(ssl_port) +
+                     "/index");
     svr.stop();
   });
 
@@ -10655,7 +10674,8 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
   });
 
   thread t = thread([&]() { ASSERT_TRUE(svr.listen("127.0.0.1", PORT)); });
-  thread t2 = thread([&]() { ASSERT_TRUE(ssl_svr.listen("127.0.0.1", 1235)); });
+  thread t2 =
+      thread([&]() { ASSERT_TRUE(ssl_svr.listen("127.0.0.1", ssl_port)); });
   auto se = detail::scope_exit([&] {
     t2.join();
     t.join();
@@ -10677,10 +10697,13 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
 }
 
 TEST(SSLClientRedirectTest, CertFile) {
+  auto ssl_port = PORT + 1;
+
   SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
   ASSERT_TRUE(ssl_svr1.is_valid());
   ssl_svr1.Get("/index", [&](const Request &, Response &res) {
-    res.set_redirect("https://127.0.0.1:1235/index");
+    res.set_redirect("https://127.0.0.1:" + std::to_string(ssl_port) +
+                     "/index");
     ssl_svr1.stop();
   });
 
@@ -10693,7 +10716,7 @@ TEST(SSLClientRedirectTest, CertFile) {
 
   thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); });
   thread t2 =
-      thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", 1235)); });
+      thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", ssl_port)); });
   auto se = detail::scope_exit([&] {
     t2.join();
     t.join();
@@ -10774,7 +10797,8 @@ TEST(MultipartFormDataTest, LargeData) {
     }
   });
 
-  auto t = std::thread([&]() { svr.listen(HOST, 8080); });
+  auto port = svr.bind_to_any_port(HOST);
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     t.join();
@@ -10788,7 +10812,7 @@ TEST(MultipartFormDataTest, LargeData) {
     std::stringstream buffer;
     buffer << data;
 
-    Client cli("https://localhost:8080");
+    SSLClient cli(HOST, port);
     cli.enable_server_certificate_verification(false);
 
     UploadFormDataItems items{
@@ -10921,7 +10945,8 @@ TEST(MultipartFormDataTest, DataProviderItems) {
     EXPECT_EQ(items[3].content_type, "");
   });
 
-  auto t = std::thread([&]() { svr.listen("localhost", 8080); });
+  auto port = svr.bind_to_any_port("localhost");
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     t.join();
@@ -10931,7 +10956,7 @@ TEST(MultipartFormDataTest, DataProviderItems) {
   svr.wait_until_ready();
 
   {
-    Client cli("https://localhost:8080");
+    SSLClient cli("localhost", port);
     cli.enable_server_certificate_verification(false);
 
     UploadFormDataItems items{
@@ -11122,7 +11147,8 @@ TEST(MultipartFormDataTest, PostCustomBoundary) {
     }
   });
 
-  auto t = std::thread([&]() { svr.listen("localhost", 8080); });
+  auto port = svr.bind_to_any_port("localhost");
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     t.join();
@@ -11136,7 +11162,7 @@ TEST(MultipartFormDataTest, PostCustomBoundary) {
     std::stringstream buffer;
     buffer << data;
 
-    Client cli("https://localhost:8080");
+    SSLClient cli("localhost", port);
     cli.enable_server_certificate_verification(false);
 
     UploadFormDataItems items{
@@ -11205,7 +11231,8 @@ TEST(MultipartFormDataTest, PutFormData) {
     }
   });
 
-  auto t = std::thread([&]() { svr.listen("localhost", 8080); });
+  auto port = svr.bind_to_any_port("localhost");
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     t.join();
@@ -11219,7 +11246,7 @@ TEST(MultipartFormDataTest, PutFormData) {
     std::stringstream buffer;
     buffer << data;
 
-    Client cli("https://localhost:8080");
+    SSLClient cli("localhost", port);
     cli.enable_server_certificate_verification(false);
 
     UploadFormDataItems items{
@@ -11269,7 +11296,8 @@ TEST(MultipartFormDataTest, PutFormDataCustomBoundary) {
             }
           });
 
-  auto t = std::thread([&]() { svr.listen("localhost", 8080); });
+  auto port = svr.bind_to_any_port("localhost");
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
   auto se = detail::scope_exit([&] {
     svr.stop();
     t.join();
@@ -11283,7 +11311,7 @@ TEST(MultipartFormDataTest, PutFormDataCustomBoundary) {
     std::stringstream buffer;
     buffer << data;
 
-    Client cli("https://localhost:8080");
+    SSLClient cli("localhost", port);
     cli.enable_server_certificate_verification(false);
 
     UploadFormDataItems items{
@@ -13176,7 +13204,8 @@ protected:
       }
       res.set_content(body, "text/plain");
     });
-    thread_ = std::thread([this]() { svr_.listen("127.0.0.1", 8787); });
+    port_ = svr_.bind_to_any_port("127.0.0.1");
+    thread_ = std::thread([this]() { svr_.listen_after_bind(); });
     svr_.wait_until_ready();
   }
   void TearDown() override {
@@ -13185,17 +13214,18 @@ protected:
   }
   Server svr_;
   std::thread thread_;
+  int port_ = 0;
 };
 
 TEST_F(OpenStreamTest, Basic) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/hello");
   EXPECT_TRUE(handle.is_valid());
   EXPECT_EQ("Hello World!", read_all(handle));
 }
 
 TEST_F(OpenStreamTest, SmallBuffer) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/hello");
   std::string result;
   char buf[4];
@@ -13206,14 +13236,15 @@ TEST_F(OpenStreamTest, SmallBuffer) {
 }
 
 TEST_F(OpenStreamTest, DefaultHeaders) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
 
   // open_stream GET should include Host, User-Agent and Accept-Encoding
   {
     auto handle = cli.open_stream("GET", "/echo-headers");
     ASSERT_TRUE(handle.is_valid());
     auto body = read_all(handle);
-    EXPECT_NE(body.find("Host:127.0.0.1:8787"), std::string::npos);
+    EXPECT_NE(body.find("Host:127.0.0.1:" + std::to_string(port_)),
+              std::string::npos);
     EXPECT_NE(body.find("User-Agent:cpp-httplib/" CPPHTTPLIB_VERSION),
               std::string::npos);
     EXPECT_NE(body.find("Accept-Encoding:"), std::string::npos);
@@ -13251,7 +13282,7 @@ TEST_F(OpenStreamTest, DefaultHeaders) {
 }
 
 TEST_F(OpenStreamTest, Large) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/large");
   EXPECT_EQ(10000u, read_all(handle).size());
 }
@@ -13263,7 +13294,7 @@ TEST_F(OpenStreamTest, ConnectionError) {
 }
 
 TEST_F(OpenStreamTest, Chunked) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/chunked");
   EXPECT_TRUE(handle.response && handle.response->get_header_value(
                                      "Transfer-Encoding") == "chunked");
@@ -13271,7 +13302,7 @@ TEST_F(OpenStreamTest, Chunked) {
 }
 
 TEST_F(OpenStreamTest, ProhibitedTrailersAreIgnored_Stream) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle =
       cli.open_stream("GET", "/streamed-chunked-with-prohibited-trailer");
   ASSERT_TRUE(handle.is_valid());
@@ -13304,7 +13335,7 @@ TEST_F(OpenStreamTest, ProhibitedTrailersAreIgnored_Stream) {
 
 #ifdef CPPHTTPLIB_ZLIB_SUPPORT
 TEST_F(OpenStreamTest, Gzip) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/compressible", {},
                                 {{"Accept-Encoding", "gzip"}});
   EXPECT_EQ("gzip", handle.response->get_header_value("Content-Encoding"));
@@ -13314,7 +13345,7 @@ TEST_F(OpenStreamTest, Gzip) {
 
 #ifdef CPPHTTPLIB_BROTLI_SUPPORT
 TEST_F(OpenStreamTest, Brotli) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle =
       cli.open_stream("GET", "/compressible", {}, {{"Accept-Encoding", "br"}});
   EXPECT_EQ("br", handle.response->get_header_value("Content-Encoding"));
@@ -13324,7 +13355,7 @@ TEST_F(OpenStreamTest, Brotli) {
 
 #ifdef CPPHTTPLIB_ZSTD_SUPPORT
 TEST_F(OpenStreamTest, Zstd) {
-  Client cli("127.0.0.1", 8787);
+  Client cli("127.0.0.1", port_);
   auto handle = cli.open_stream("GET", "/compressible", {},
                                 {{"Accept-Encoding", "zstd"}});
   EXPECT_EQ("zstd", handle.response->get_header_value("Content-Encoding"));
@@ -13365,7 +13396,8 @@ protected:
             return true;
           });
     });
-    thread_ = std::thread([this]() { svr_.listen("127.0.0.1", 8788); });
+    port_ = svr_.bind_to_any_port("127.0.0.1");
+    thread_ = std::thread([this]() { svr_.listen_after_bind(); });
     svr_.wait_until_ready();
   }
   void TearDown() override {
@@ -13374,10 +13406,11 @@ protected:
   }
   SSLServer svr_;
   std::thread thread_;
+  int port_ = 0;
 };
 
 TEST_F(SSLOpenStreamTest, Basic) {
-  SSLClient cli("127.0.0.1", 8788);
+  SSLClient cli("127.0.0.1", port_);
   cli.enable_server_certificate_verification(false);
   auto handle = cli.open_stream("GET", "/hello");
   ASSERT_TRUE(handle.is_valid());
@@ -13385,7 +13418,7 @@ TEST_F(SSLOpenStreamTest, Basic) {
 }
 
 TEST_F(SSLOpenStreamTest, Chunked) {
-  SSLClient cli("127.0.0.1", 8788);
+  SSLClient cli("127.0.0.1", port_);
   cli.enable_server_certificate_verification(false);
 
   auto handle = cli.open_stream("GET", "/chunked");
@@ -13399,7 +13432,7 @@ TEST_F(SSLOpenStreamTest, Chunked) {
 }
 
 TEST_F(SSLOpenStreamTest, Post) {
-  SSLClient cli("127.0.0.1", 8788);
+  SSLClient cli("127.0.0.1", port_);
   cli.enable_server_certificate_verification(false);
 
   auto handle =
@@ -13413,7 +13446,7 @@ TEST_F(SSLOpenStreamTest, Post) {
 }
 
 TEST_F(SSLOpenStreamTest, PostChunked) {
-  SSLClient cli("127.0.0.1", 8788);
+  SSLClient cli("127.0.0.1", port_);
   cli.enable_server_certificate_verification(false);
 
   auto handle = cli.open_stream("POST", "/chunked-response", {}, {},
@@ -13738,7 +13771,8 @@ protected:
     svr_.Post("/echo", [](const httplib::Request &req, httplib::Response &res) {
       res.set_content(req.body, "text/plain");
     });
-    thread_ = std::thread([this]() { svr_.listen("127.0.0.1", 8803); });
+    port_ = svr_.bind_to_any_port("127.0.0.1");
+    thread_ = std::thread([this]() { svr_.listen_after_bind(); });
     svr_.wait_until_ready();
   }
   void TearDown() override {
@@ -13747,10 +13781,11 @@ protected:
   }
   httplib::SSLServer svr_{"cert.pem", "key.pem"};
   std::thread thread_;
+  int port_ = 0;
 };
 
 TEST_F(SSLStreamApiTest, GetAndPost) {
-  httplib::SSLClient cli("127.0.0.1", 8803);
+  httplib::SSLClient cli("127.0.0.1", port_);
   cli.enable_server_certificate_verification(false);
   auto get = httplib::stream::Get(cli, "/hello");
   EXPECT_EQ("Hello SSL!", read_body(get));