From 50ee588b09a2ed3e91d79a80ba71b10689422446 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Tue, 23 Jun 2026 19:52:45 +0900 Subject: [PATCH 1/5] Fix the macOS CI matrix and split unit tests from integration tests --- .github/workflows/test.yml | 15 +++++++++++++-- async_postgres.nimble | 6 +++++- tests/all_tests.nim | 10 ++++------ tests/all_tests_unit.nim | 11 +++++++++++ tests/test_keepalive.nim | 17 +++++++++++------ 5 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 tests/all_tests_unit.nim diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d5381a2..f67ed995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,8 @@ permissions: jobs: tests: - runs-on: ubuntu-latest - timeout-minutes: 30 + runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: matrix: os: @@ -68,15 +68,24 @@ jobs: - name: Generate test certificates run: bash tests/gen_certs.sh + # macOS runners have no Docker, so PostgreSQL is only available on Linux. - name: Start PostgreSQL + if: runner.os == 'Linux' run: docker compose up -d --wait - name: Install chronos run: nimble install chronos -y + # Linux runs the full suite against the live PostgreSQL above. - name: Run tests + if: runner.os == 'Linux' run: nimble test -y + # macOS has no PostgreSQL; run only the unit and mock-server tests. + - name: Run unit tests + if: runner.os != 'Linux' + run: nimble test_unit -y + - name: Install async_postgres run: | rm -rf ~/.nimble/pkgs2/async_postgres-* @@ -88,6 +97,8 @@ jobs: - name: Compile examples (chronos) run: for f in examples/*.nim; do nim c -d:asyncBackend=chronos "$f"; done + # Doc generation is platform-independent; run it once (on Linux). - name: Gen docs + if: runner.os == 'Linux' run: | nim doc --project --index:on --outdir:./htmldocs ./async_postgres.nim diff --git a/async_postgres.nimble b/async_postgres.nimble index bbdafad2..fb32c314 100644 --- a/async_postgres.nimble +++ b/async_postgres.nimble @@ -11,6 +11,10 @@ requires "nim >= 2.2.4" requires "nimcrypto >= 0.6.0" requires "checksums >= 0.2.0" -task test, "test": +task test, "run the full suite (requires a live PostgreSQL on 127.0.0.1:15432)": exec "nim c -d:asyncBackend=asyncdispatch -r tests/all_tests.nim" exec "nim c -d:asyncBackend=chronos -r tests/all_tests.nim" + +task test_unit, "run unit and mock-server tests only (no PostgreSQL required)": + exec "nim c -d:asyncBackend=asyncdispatch -r tests/all_tests_unit.nim" + exec "nim c -d:asyncBackend=chronos -r tests/all_tests_unit.nim" diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 2bff7076..29d444ed 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -1,8 +1,6 @@ +## Full test suite: the unit/mock tests plus the live-PostgreSQL integration +## tests. Requires a running PostgreSQL (see docker-compose.yml). For a run +## that needs no database, use all_tests_unit.nim instead. {.push warning[UnusedImport]: off.} -import - test_abandonment_e2e, test_advisory_lock, test_auth, test_cancel_e2e, test_dsn, - test_e2e, test_fill_recvbuf, test_keepalive, test_largeobject, test_listen_reconnect, - test_network_failure, test_physical_replication, test_pool, test_protocol, - test_protocol_fuzz, test_replication, test_replication_keepalive, test_rowdata, - test_session_attrs, test_sql, test_ssl, test_tracing, test_types, test_pool_cluster +import all_tests_unit, all_tests_integration {.pop.} diff --git a/tests/all_tests_unit.nim b/tests/all_tests_unit.nim new file mode 100644 index 00000000..10412099 --- /dev/null +++ b/tests/all_tests_unit.nim @@ -0,0 +1,11 @@ +## Unit and in-process mock-server tests. None of these require a live +## PostgreSQL: every test either exercises pure logic or connects to an +## in-process mock server on an ephemeral port. They therefore run anywhere, +## including CI hosts without Docker (e.g. macOS runners). +{.push warning[UnusedImport]: off.} +import + test_auth, test_dsn, test_fill_recvbuf, test_keepalive, test_listen_reconnect, + test_network_failure, test_physical_replication, test_pool, test_pool_cluster, + test_protocol, test_protocol_fuzz, test_replication, test_replication_keepalive, + test_rowdata, test_session_attrs, test_sql, test_ssl, test_types +{.pop.} diff --git a/tests/test_keepalive.nim b/tests/test_keepalive.nim index c91b57dd..57bceb01 100644 --- a/tests/test_keepalive.nim +++ b/tests/test_keepalive.nim @@ -15,6 +15,11 @@ suite "configureKeepalive": doAssert fd != SocketHandle(-1), "socket() failed" fd + proc keepaliveEnabled(fd: SocketHandle): bool = + # macOS/BSD getsockopt returns the SO_KEEPALIVE flag bit (8), Linux returns 1, + # so treat any non-zero value as "enabled". + getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) != 0 + test "keepAlive=false does not set SO_KEEPALIVE": let fd = makeSocket() defer: @@ -22,7 +27,7 @@ suite "configureKeepalive": var config = ConnConfig() config.keepAlive = false configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 0 + check not keepaliveEnabled(fd) test "keepAlive=true sets SO_KEEPALIVE": let fd = makeSocket() @@ -31,7 +36,7 @@ suite "configureKeepalive": var config = ConnConfig() config.keepAlive = true configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 1 + check keepaliveEnabled(fd) test "keepAlive with idle/interval/count": let fd = makeSocket() @@ -43,7 +48,7 @@ suite "configureKeepalive": config.keepAliveInterval = 7 config.keepAliveCount = 3 configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 1 + check keepaliveEnabled(fd) when defined(linux): check getIntSockOpt(fd, cint(posix.IPPROTO_TCP), TCP_KEEPIDLE) == 42 check getIntSockOpt(fd, cint(posix.IPPROTO_TCP), TCP_KEEPINTVL) == 7 @@ -63,7 +68,7 @@ suite "configureKeepalive": config.keepAliveInterval = 0 config.keepAliveCount = 0 configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 1 + check keepaliveEnabled(fd) test "keepAlive=false with timing params does not set SO_KEEPALIVE": let fd = makeSocket() @@ -75,7 +80,7 @@ suite "configureKeepalive": config.keepAliveInterval = 10 config.keepAliveCount = 3 configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 0 + check not keepaliveEnabled(fd) test "partial timing (idle only)": let fd = makeSocket() @@ -85,7 +90,7 @@ suite "configureKeepalive": config.keepAlive = true config.keepAliveIdle = 99 configureKeepalive(fd, config) - check getIntSockOpt(fd, SOL_SOCKET, SO_KEEPALIVE) == 1 + check keepaliveEnabled(fd) when defined(linux): check getIntSockOpt(fd, cint(posix.IPPROTO_TCP), TCP_KEEPIDLE) == 99 elif defined(macosx): From f4ad4cbfa36907d928f99ecb356a87da72391b90 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Tue, 23 Jun 2026 19:59:11 +0900 Subject: [PATCH 2/5] fix --- tests/all_tests_integration.nim | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/all_tests_integration.nim diff --git a/tests/all_tests_integration.nim b/tests/all_tests_integration.nim new file mode 100644 index 00000000..736ef6c5 --- /dev/null +++ b/tests/all_tests_integration.nim @@ -0,0 +1,8 @@ +## End-to-end tests that require a live PostgreSQL at 127.0.0.1:15432 +## (started via docker-compose.yml). Run these only where Docker/PostgreSQL +## is available; the unit/mock suite lives in all_tests_unit.nim. +{.push warning[UnusedImport]: off.} +import + test_abandonment_e2e, test_advisory_lock, test_cancel_e2e, test_e2e, test_largeobject, + test_tracing +{.pop.} From 10f78280856c91cc6e7d5d2f189e91f7957c8352 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Mon, 13 Jul 2026 19:23:41 +0900 Subject: [PATCH 3/5] fix: lazy-load SSL_set1_host so macOS test binary starts --- async_postgres/pg_connection/ssl.nim | 28 ++++++++++++++++++++++------ tests/test_ssl.nim | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/async_postgres/pg_connection/ssl.nim b/async_postgres/pg_connection/ssl.nim index ce467b08..dfb3ad10 100644 --- a/async_postgres/pg_connection/ssl.nim +++ b/async_postgres/pg_connection/ssl.nim @@ -31,12 +31,11 @@ when hasAsyncDispatch and defined(ssl): # requesting only `SSL_VERIFY_PEER` (chain verification), sslVerifyFull would # accept any CA-trusted cert for any host. So tell OpenSSL the expected identity # *before* the handshake; it then matches the cert during its own verification - # and fails closed. std/openssl doesn't bind these, so declare them. - proc SSL_set1_host( - ssl: SslPtr, hostname: cstring - ): cint {.cdecl, dynlib: DLLSSLName, importc.} - + # and fails closed. std/openssl doesn't bind these symbols, so resolve them + # ourselves. type + SslSet1HostFn = + proc(ssl: SslPtr, hostname: cstring): cint {.cdecl, gcsafe, raises: [].} SslGet0ParamFn = proc(ssl: SslPtr): pointer {.cdecl, gcsafe, raises: [].} X509SetIpAscFn = proc(param: pointer, ipasc: cstring): cint {.cdecl, gcsafe, raises: [].} @@ -45,11 +44,21 @@ when hasAsyncDispatch and defined(ssl): # binding would abort the process at startup. Resolve lazily and let callers # handle nil. var + sslSet1HostFn: SslSet1HostFn + sslSet1HostResolved: bool sslGet0ParamFn: SslGet0ParamFn sslGet0ParamResolved: bool x509SetIpAscFn: X509SetIpAscFn x509SetIpAscResolved: bool + proc sslSet1Host*(): SslSet1HostFn = + if not sslSet1HostResolved: + let lib = loadLibPattern(DLLSSLName) + if lib != nil: + sslSet1HostFn = cast[SslSet1HostFn](symAddr(lib, "SSL_set1_host")) + sslSet1HostResolved = true + sslSet1HostFn + proc sslGet0Param*(): SslGet0ParamFn = if not sslGet0ParamResolved: let lib = loadLibPattern(DLLSSLName) @@ -90,7 +99,14 @@ when hasAsyncDispatch and defined(ssl): ) fn(getParam(sslHandle), host.cstring) else: - SSL_set1_host(sslHandle, host.cstring) + let fn = sslSet1Host() + if fn == nil: + raise newException( + PgConnectionError, + "sslmode=verify-full: libssl does not export SSL_set1_host; " & + "cannot verify " & host, + ) + fn(sslHandle, host.cstring) if ok != 1: raise newException( PgConnectionError, diff --git a/tests/test_ssl.nim b/tests/test_ssl.nim index 8d78502e..922c4568 100644 --- a/tests/test_ssl.nim +++ b/tests/test_ssl.nim @@ -1169,7 +1169,7 @@ when hasAsyncDispatch and defined(ssl): test "installs the DNS host on the SSL handle": resolveX509TestSyms() let getParam = sslGet0Param() - if x509GetHostFn == nil or getParam == nil: + if x509GetHostFn == nil or getParam == nil or sslSet1Host() == nil: skip() else: let ctx = newContext(verifyMode = CVerifyNone) From 00ccda9320d9371e3b9514aec85a3e51ee21bb03 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Mon, 13 Jul 2026 20:22:54 +0900 Subject: [PATCH 4/5] Fix appendDnCallback --- async_postgres/pg_bearssl.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async_postgres/pg_bearssl.nim b/async_postgres/pg_bearssl.nim index 5374d2f5..fb25847d 100644 --- a/async_postgres/pg_bearssl.nim +++ b/async_postgres/pg_bearssl.nim @@ -23,7 +23,7 @@ when hasChronos: backing*: seq[seq[byte]] ## Owns memory pointed to by trust anchor fields proc appendDnCallback( - ctx: X509ClassPointerConst, buf: ConstPtrByte, len: csize_t + ctx: pointer, buf: pointer, len: csize_t ) {.cdecl, gcsafe, noSideEffect, raises: [].} = ## DN accumulation callback let s = cast[ptr seq[byte]](ctx) From 0ed0e38a754e79f9dc3b59c5e5470f3927776f26 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Mon, 13 Jul 2026 21:19:51 +0900 Subject: [PATCH 5/5] fix --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4b8facf..f67ed995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,7 @@ jobs: matrix: os: - 'ubuntu-latest' + - 'macOS-latest' nim-version: - '2.2.4' - 'stable'