test.yaml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. name: test
  2. on:
  3. push:
  4. pull_request:
  5. workflow_dispatch:
  6. inputs:
  7. gtest_filter:
  8. description: 'Google Test filter'
  9. test_linux:
  10. description: 'Test on Linux'
  11. type: boolean
  12. default: true
  13. test_macos:
  14. description: 'Test on MacOS'
  15. type: boolean
  16. default: true
  17. test_windows:
  18. description: 'Test on Windows'
  19. type: boolean
  20. default: true
  21. concurrency:
  22. group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
  23. cancel-in-progress: true
  24. env:
  25. # Exclude *_Online tests by default — they hit external services and flake on
  26. # CI runners. Run with workflow_dispatch + a custom filter to include them.
  27. GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }}
  28. jobs:
  29. style-check:
  30. runs-on: ubuntu-latest
  31. if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
  32. continue-on-error: true
  33. steps:
  34. - name: checkout
  35. uses: actions/checkout@v4
  36. - name: run style check
  37. run: |
  38. clang-format --version
  39. cd test && make style_check
  40. build-and-test-on-32bit:
  41. runs-on: ubuntu-latest
  42. if: >
  43. (github.event_name == 'push') ||
  44. (github.event_name == 'pull_request' &&
  45. github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
  46. (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
  47. strategy:
  48. matrix:
  49. config:
  50. - arch_flags: -m32
  51. arch_suffix: :i386
  52. name: (32-bit)
  53. steps:
  54. - name: checkout
  55. uses: actions/checkout@v4
  56. - name: install libraries
  57. run: |
  58. sudo dpkg --add-architecture i386
  59. sudo apt-get update
  60. sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \
  61. libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
  62. zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
  63. libzstd-dev${{ matrix.config.arch_suffix }}
  64. - name: build and run tests
  65. run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
  66. ubuntu:
  67. runs-on: ubuntu-latest
  68. if: >
  69. (github.event_name == 'push') ||
  70. (github.event_name == 'pull_request' &&
  71. github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
  72. (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
  73. strategy:
  74. fail-fast: false
  75. matrix:
  76. tls_backend: [openssl, mbedtls, wolfssl]
  77. # TODO: mbedTLS jobs hit pre-existing flaky failures (timing-sensitive
  78. # ServerTest cases under ASAN+mbedTLS). Allow them to fail without
  79. # blocking until the underlying flakiness is investigated.
  80. continue-on-error: ${{ matrix.tls_backend == 'mbedtls' }}
  81. name: ubuntu (${{ matrix.tls_backend }})
  82. steps:
  83. - name: checkout
  84. uses: actions/checkout@v4
  85. - name: install common libraries
  86. run: |
  87. sudo apt-get update
  88. sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
  89. - name: install OpenSSL
  90. if: matrix.tls_backend == 'openssl'
  91. run: sudo apt-get install -y libssl-dev
  92. - name: install Mbed TLS
  93. if: matrix.tls_backend == 'mbedtls'
  94. run: sudo apt-get install -y libmbedtls-dev
  95. - name: install wolfSSL
  96. if: matrix.tls_backend == 'wolfssl'
  97. run: sudo apt-get install -y libwolfssl-dev
  98. - name: build and run tests (OpenSSL)
  99. if: matrix.tls_backend == 'openssl'
  100. run: cd test && make test_split && make test_openssl_parallel
  101. env:
  102. LSAN_OPTIONS: suppressions=lsan_suppressions.txt
  103. - name: build and run tests (Mbed TLS)
  104. if: matrix.tls_backend == 'mbedtls'
  105. run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
  106. - name: build and run tests (wolfSSL)
  107. if: matrix.tls_backend == 'wolfssl'
  108. run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
  109. - name: run fuzz test target
  110. if: matrix.tls_backend == 'openssl'
  111. run: cd test && make fuzz_test
  112. - name: build and run WebSocket heartbeat test
  113. if: matrix.tls_backend == 'openssl'
  114. run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
  115. - name: build and run ThreadPool test
  116. run: cd test && make test_thread_pool && ./test_thread_pool
  117. # Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
  118. # On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
  119. # DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
  120. # When gai_suspend() hits the connection timeout, gai_cancel() is called
  121. # but does not block; the resolver worker can later write back into the
  122. # destroyed stack frame. To make the worker actually reach that write,
  123. # the test job runs a loopback UDP responder (test/dns_test_fixture.py)
  124. # that delays its reply past the test's 1s timeout, and uses an iptables
  125. # NAT rule so glibc's lookups land on that fixture instead of a real
  126. # nameserver. With ASAN's detect_stack_use_after_return enabled, the
  127. # late write-back is reported as a stack-use-after-return.
  128. issue-2431-repro:
  129. runs-on: ubuntu-latest
  130. if: >
  131. (github.event_name == 'push') ||
  132. (github.event_name == 'pull_request' &&
  133. github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
  134. (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
  135. name: issue-2431 repro (Linux + ASAN)
  136. # Bound the whole job in case anything in the test harness hangs
  137. # unexpectedly. With the fixture in place a normal run is well under
  138. # a minute either way (ASAN abort on broken HEAD, clean pass on fix).
  139. timeout-minutes: 5
  140. env:
  141. DNS_FIXTURE_PORT: "15353"
  142. DNS_FIXTURE_DELAY: "3"
  143. steps:
  144. - name: checkout
  145. uses: actions/checkout@v4
  146. - name: install libraries
  147. run: |
  148. sudo apt-get update
  149. sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
  150. libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
  151. - name: start loopback DNS test fixture
  152. run: |
  153. # Force glibc through its DNS code path: Ubuntu's default
  154. # nsswitch short-circuits to NOTFOUND through mdns4_minimal,
  155. # which would skip the buggy code entirely.
  156. sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
  157. # Run the loopback fixture (delayed UDP responder).
  158. python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
  159. >/tmp/dns_fixture.log 2>&1 &
  160. echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
  161. # Wait for the fixture to start listening.
  162. for _ in $(seq 1 50); do
  163. if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
  164. break
  165. fi
  166. sleep 0.1
  167. done
  168. ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
  169. || { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
  170. # Send the test process's DNS lookups to the loopback fixture.
  171. # NAT only the local OUTPUT chain; conntrack handles the reply path.
  172. sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
  173. -j REDIRECT --to-port "$DNS_FIXTURE_PORT"
  174. # Sanity check: a query must take at least the fixture delay
  175. # and resolve to NXDOMAIN (proving traffic reaches the fixture).
  176. start=$(date +%s)
  177. getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
  178. elapsed=$(( $(date +%s) - start ))
  179. if [ "$elapsed" -lt 2 ]; then
  180. echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
  181. exit 1
  182. fi
  183. echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
  184. - name: build test binary
  185. run: cd test && make test
  186. - name: run GetAddrInfoAsyncCancelTest
  187. run: |
  188. cd test
  189. ARCH=$(uname -m)
  190. CPPHTTPLIB_TEST_ISSUE_2431=1 \
  191. ASAN_OPTIONS=detect_stack_use_after_return=1 \
  192. LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
  193. setarch "$ARCH" -R \
  194. ./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
  195. - name: tear down test fixture
  196. if: always()
  197. run: |
  198. sudo iptables -t nat -F OUTPUT || true
  199. if [ -f /tmp/dns_fixture.pid ]; then
  200. sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
  201. fi
  202. macos:
  203. runs-on: macos-latest
  204. if: >
  205. (github.event_name == 'push') ||
  206. (github.event_name == 'pull_request' &&
  207. github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
  208. (github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
  209. strategy:
  210. fail-fast: false
  211. matrix:
  212. tls_backend: [openssl, mbedtls, wolfssl]
  213. # See ubuntu job above.
  214. continue-on-error: ${{ matrix.tls_backend == 'mbedtls' }}
  215. name: macos (${{ matrix.tls_backend }})
  216. steps:
  217. - name: checkout
  218. uses: actions/checkout@v4
  219. - name: install Mbed TLS
  220. if: matrix.tls_backend == 'mbedtls'
  221. run: brew install mbedtls@3
  222. - name: install wolfSSL
  223. if: matrix.tls_backend == 'wolfssl'
  224. run: brew install wolfssl
  225. - name: build and run tests (OpenSSL)
  226. if: matrix.tls_backend == 'openssl'
  227. run: cd test && make test_split && make test_openssl_parallel
  228. env:
  229. LSAN_OPTIONS: suppressions=lsan_suppressions.txt
  230. - name: build and run tests (Mbed TLS)
  231. if: matrix.tls_backend == 'mbedtls'
  232. run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
  233. - name: build and run tests (wolfSSL)
  234. if: matrix.tls_backend == 'wolfssl'
  235. run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
  236. - name: run fuzz test target
  237. if: matrix.tls_backend == 'openssl'
  238. run: cd test && make fuzz_test
  239. - name: build and run WebSocket heartbeat test
  240. if: matrix.tls_backend == 'openssl'
  241. run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
  242. - name: build and run ThreadPool test
  243. run: cd test && make test_thread_pool && ./test_thread_pool
  244. windows:
  245. runs-on: windows-latest
  246. if: >
  247. (github.event_name == 'push') ||
  248. (github.event_name == 'pull_request' &&
  249. github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
  250. (github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true')
  251. strategy:
  252. fail-fast: false
  253. matrix:
  254. config:
  255. - with_ssl: false
  256. compiled: false
  257. run_tests: true
  258. name: without SSL
  259. - with_ssl: true
  260. compiled: false
  261. run_tests: true
  262. name: with SSL
  263. - with_ssl: false
  264. compiled: true
  265. run_tests: false
  266. name: compiled
  267. name: windows ${{ matrix.config.name }}
  268. steps:
  269. - name: Prepare Git for Checkout on Windows
  270. run: |
  271. git config --global core.autocrlf false
  272. git config --global core.eol lf
  273. - name: Checkout
  274. uses: actions/checkout@v4
  275. - name: Export GitHub Actions cache environment variables
  276. uses: actions/github-script@v7
  277. with:
  278. script: |
  279. core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
  280. core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
  281. - name: Setup msbuild on windows
  282. uses: microsoft/setup-msbuild@v2
  283. - name: Cache vcpkg packages
  284. id: vcpkg-cache
  285. uses: actions/cache@v4
  286. with:
  287. path: C:/vcpkg/installed
  288. key: vcpkg-installed-windows-gtest-curl-zlib-brotli-zstd
  289. - name: Install vcpkg dependencies
  290. if: steps.vcpkg-cache.outputs.cache-hit != 'true'
  291. run: vcpkg install gtest curl zlib brotli zstd
  292. - name: Install OpenSSL
  293. if: ${{ matrix.config.with_ssl }}
  294. run: choco install openssl
  295. - name: Configure CMake ${{ matrix.config.name }}
  296. run: >
  297. cmake -B build -S .
  298. -DCMAKE_BUILD_TYPE=Release
  299. -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake
  300. -DHTTPLIB_TEST=ON
  301. -DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }}
  302. -DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
  303. -DHTTPLIB_REQUIRE_ZLIB=ON
  304. -DHTTPLIB_REQUIRE_BROTLI=ON
  305. -DHTTPLIB_REQUIRE_ZSTD=ON
  306. -DHTTPLIB_REQUIRE_OPENSSL=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
  307. - name: Build ${{ matrix.config.name }}
  308. run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
  309. - name: Run tests ${{ matrix.config.name }}
  310. if: ${{ matrix.config.run_tests }}
  311. shell: pwsh
  312. working-directory: build/test
  313. run: |
  314. $shards = 4
  315. $procs = @()
  316. for ($i = 0; $i -lt $shards; $i++) {
  317. $log = "shard_${i}.log"
  318. $procs += Start-Process -FilePath ./Release/httplib-test.exe `
  319. -ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '-*_Online' }}" `
  320. -NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
  321. -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
  322. }
  323. $procs | Wait-Process
  324. $failed = $false
  325. for ($i = 0; $i -lt $shards; $i++) {
  326. $log = "shard_${i}.log"
  327. $proc = $procs[$i]
  328. $hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet
  329. $hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet
  330. if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) {
  331. $passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line
  332. Write-Host "Shard ${i}: $passed"
  333. } else {
  334. Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ==="
  335. Get-Content $log
  336. if (Test-Path "${log}.err") { Get-Content "${log}.err" }
  337. $failed = $true
  338. }
  339. }
  340. if ($failed) { exit 1 }
  341. Write-Host "All shards passed."
  342. env:
  343. VCPKG_ROOT: "C:/vcpkg"
  344. VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"