name: test on: push: pull_request: workflow_dispatch: inputs: gtest_filter: description: 'Google Test filter' test_linux: description: 'Test on Linux' type: boolean default: true test_macos: description: 'Test on MacOS' type: boolean default: true test_windows: description: 'Test on Windows' type: boolean default: true concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true env: # Exclude *_Online tests by default — they hit external services and flake on # CI runners. Run with workflow_dispatch + a custom filter to include them. GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }} jobs: style-check: runs-on: ubuntu-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name continue-on-error: true steps: - name: checkout uses: actions/checkout@v4 - name: run style check run: | clang-format --version cd test && make style_check build-and-test-on-32bit: runs-on: ubuntu-latest if: > (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') strategy: matrix: config: - arch_flags: -m32 arch_suffix: :i386 name: (32-bit) steps: - name: checkout uses: actions/checkout@v4 - name: install libraries run: | sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \ libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \ zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \ libzstd-dev${{ matrix.config.arch_suffix }} - name: build and run tests run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}" ubuntu: runs-on: ubuntu-latest if: > (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') strategy: fail-fast: false matrix: tls_backend: [openssl, mbedtls, wolfssl] name: ubuntu (${{ matrix.tls_backend }}) steps: - name: checkout uses: actions/checkout@v4 - name: install common libraries run: | sudo apt-get update sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev - name: install OpenSSL if: matrix.tls_backend == 'openssl' run: sudo apt-get install -y libssl-dev - name: install Mbed TLS if: matrix.tls_backend == 'mbedtls' run: sudo apt-get install -y libmbedtls-dev - name: install wolfSSL if: matrix.tls_backend == 'wolfssl' run: sudo apt-get install -y libwolfssl-dev - name: build and run tests (OpenSSL) if: matrix.tls_backend == 'openssl' 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 mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the # default 4 shards overload CI runners enough that timing-sensitive # ServerTest cases flake on first-request keep-alive reuse. run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel - name: build and run tests (wolfSSL) if: matrix.tls_backend == 'wolfssl' run: cd test && make test_split_wolfssl && make test_wolfssl_parallel - name: run fuzz test target if: matrix.tls_backend == 'openssl' run: cd test && make fuzz_test - name: build and run WebSocket heartbeat test if: matrix.tls_backend == 'openssl' run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat - name: build and run ThreadPool test run: cd test && make test_thread_pool && ./test_thread_pool # Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431. # On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous # DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb. # When gai_suspend() hits the connection timeout, gai_cancel() is called # but does not block; the resolver worker can later write back into the # destroyed stack frame. To make the worker actually reach that write, # the test job runs a loopback UDP responder (test/dns_test_fixture.py) # that delays its reply past the test's 1s timeout, and uses an iptables # NAT rule so glibc's lookups land on that fixture instead of a real # nameserver. With ASAN's detect_stack_use_after_return enabled, the # late write-back is reported as a stack-use-after-return. issue-2431-repro: runs-on: ubuntu-latest if: > (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') name: issue-2431 repro (Linux + ASAN) # Bound the whole job in case anything in the test harness hangs # unexpectedly. With the fixture in place a normal run is well under # a minute either way (ASAN abort on broken HEAD, clean pass on fix). timeout-minutes: 5 env: DNS_FIXTURE_PORT: "15353" DNS_FIXTURE_DELAY: "3" steps: - name: checkout uses: actions/checkout@v4 - name: install libraries run: | sudo apt-get update sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \ libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2 - name: start loopback DNS test fixture run: | # Force glibc through its DNS code path: Ubuntu's default # nsswitch short-circuits to NOTFOUND through mdns4_minimal, # which would skip the buggy code entirely. sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf # Run the loopback fixture (delayed UDP responder). python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \ >/tmp/dns_fixture.log 2>&1 & echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null # Wait for the fixture to start listening. for _ in $(seq 1 50); do if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then break fi sleep 0.1 done ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \ || { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; } # Send the test process's DNS lookups to the loopback fixture. # NAT only the local OUTPUT chain; conntrack handles the reply path. sudo iptables -t nat -I OUTPUT -p udp --dport 53 \ -j REDIRECT --to-port "$DNS_FIXTURE_PORT" # Sanity check: a query must take at least the fixture delay # and resolve to NXDOMAIN (proving traffic reaches the fixture). start=$(date +%s) getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true elapsed=$(( $(date +%s) - start )) if [ "$elapsed" -lt 2 ]; then echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2 exit 1 fi echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)" - name: build test binary run: cd test && make test - name: run GetAddrInfoAsyncCancelTest run: | cd test ARCH=$(uname -m) CPPHTTPLIB_TEST_ISSUE_2431=1 \ ASAN_OPTIONS=detect_stack_use_after_return=1 \ LSAN_OPTIONS=suppressions=lsan_suppressions.txt \ setarch "$ARCH" -R \ ./test --gtest_filter='GetAddrInfoAsyncCancelTest.*' - name: tear down test fixture if: always() run: | sudo iptables -t nat -F OUTPUT || true if [ -f /tmp/dns_fixture.pid ]; then sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true fi macos: runs-on: macos-latest if: > (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true') strategy: fail-fast: false matrix: tls_backend: [openssl, mbedtls, wolfssl] name: macos (${{ matrix.tls_backend }}) steps: - name: checkout uses: actions/checkout@v4 - name: install Mbed TLS if: matrix.tls_backend == 'mbedtls' run: brew install mbedtls@3 - name: install wolfSSL if: matrix.tls_backend == 'wolfssl' run: brew install wolfssl - name: build and run tests (OpenSSL) if: matrix.tls_backend == 'openssl' 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 mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the # default 4 shards overload CI runners enough that timing-sensitive # ServerTest cases flake on first-request keep-alive reuse. run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel - name: build and run tests (wolfSSL) if: matrix.tls_backend == 'wolfssl' run: cd test && make test_split_wolfssl && make test_wolfssl_parallel - name: run fuzz test target if: matrix.tls_backend == 'openssl' run: cd test && make fuzz_test - name: build and run WebSocket heartbeat test if: matrix.tls_backend == 'openssl' run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat - name: build and run ThreadPool test run: cd test && make test_thread_pool && ./test_thread_pool windows: runs-on: windows-latest if: > (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true') strategy: fail-fast: false matrix: config: - with_ssl: false compiled: false run_tests: true name: without SSL - with_ssl: true compiled: false run_tests: true name: with SSL - with_ssl: false compiled: true run_tests: false name: compiled name: windows ${{ matrix.config.name }} steps: - name: Prepare Git for Checkout on Windows run: | git config --global core.autocrlf false git config --global core.eol lf - name: Checkout uses: actions/checkout@v4 - name: Export GitHub Actions cache environment variables uses: actions/github-script@v7 with: script: | core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); 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 }} run: choco install openssl - name: Configure CMake ${{ matrix.config.name }} run: > cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake -DHTTPLIB_TEST=ON -DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }} -DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }} -DHTTPLIB_REQUIRE_ZLIB=ON -DHTTPLIB_REQUIRE_BROTLI=ON -DHTTPLIB_REQUIRE_ZSTD=ON -DHTTPLIB_REQUIRE_OPENSSL=${{ matrix.config.with_ssl && 'ON' || 'OFF' }} - name: Build ${{ matrix.config.name }} run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine - name: Run tests ${{ matrix.config.name }} if: ${{ matrix.config.run_tests }} 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 || '-*_Online' }}" ` -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" $proc = $procs[$i] $hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet $hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) { $passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line Write-Host "Shard ${i}: $passed" } else { Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ===" Get-Content $log if (Test-Path "${log}.err") { Get-Content "${log}.err" } $failed = $true } } if ($failed) { exit 1 } Write-Host "All shards passed." env: VCPKG_ROOT: "C:/vcpkg" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"