Skip to content

Socket test improvements - #108

Open
rnro wants to merge 6 commits into
apple:mainfrom
rnro:socket-test-improvements
Open

Socket test improvements#108
rnro wants to merge 6 commits into
apple:mainfrom
rnro:socket-test-improvements

Conversation

@rnro

@rnro rnro commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

This PR aims to make Socket tests faster and more stable, no source changes.

  • Servers now bind port 0 and report listeningPort.
  • Where the API doesn't expose the port a test helper is used to discover a free port and that is then used (discoverFreeLoopbackPorts). This is still racy, but avoids most collisions and issues when hanging tests have the port.
  • TCPClientHarness.init reports a start failure instead of dropping it with try?, so a bind failure no longer surfaces later as an unexplained timeout.
  • The refused-connection test asserts from its recorded state history rather than an inverted expectation, which XCTest can only satisfy by waiting out the whole timeout (locally 0.002s against 5.003s).
  • The split-send server blocks on the peer's FIN instead of a one second sleep.
  • The interface test which previously exhausted all FDs on the system now lowers its own RLIMIT_NOFILE instead of opening sockets until the kernel refuses. This was probably a cause of a reasonable amount of test flakiness and was slow. (locally 17s to under a millisecond).
  • Three waits are deliberately kept, each now commented with rationale

rnro added 4 commits August 21, 2026 12:00
Tests using sockets hard-coded loopback port numbers making them fragile
and could cause spurious failures (or passes) depending on other
activity on the system.
`TCPClientHarness.init` also swallowed a server start failure with `try?`,
so a bind failure surfaced later as an unexplained timeout.

The three servers now bind port 0 and report back what the kernel gave them as
`listeningPort`, and a start failure is reported where it happens. Both harnesses lose their
port parameters, which takes port bookkeeping out of all 49 callsites.

`discoverFreeLoopbackPorts(_:)` covers the peers that need a number before anything is bound
to it, such as the UDP cross-binding and the connection tests. It discovers a free port
by binding port 0 then releasing it. This is racey but has a small
window which should be more resilient than the fixed ports.
It takes several at once to avoid handing out the same port more than once.
`testTCPConnectionRefusedDeliversFailure` asserted the absence of `.ready` with an inverted
`XCTestExpectation`, which XCTest can only satisfy by letting the whole timeout elapse. The
test therefore paid its full timeout every run no matter how quickly the connection was
refused, and only covered the window it waited out.

Recording the observed state sequence and asserting `.ready` is absent from it catches the
state anywhere before the failure and dumps the sequence when it does appear. It is also
fast rather than fixed-cost (locally 0.002s against 5.003s).
`SplitSendServer` held its connection open with a one second sleep, which is a guess at how
long the client needs, and kept the accepted descriptor and its thread alive past teardown.
Blocking until `read` returns 0 is exact, and it is the idiom `TCPEchoServer` in the same
file already uses.

The `gap` between the two segments stays, and is now documented as load-bearing: it is what
makes the client process the first segment alone, which is the condition
`testTCPReceiveAtLeastSpanningTwoSegments` reproduces, and shrinking it lets the writes
coalesce so the test passes without exercising the bug. Both waits in
`testTCPNoBusyLoopAfterEOF` are documented for the same reason -- the second one is the
measurement, since no read event firing is only observable as an absence over an interval.
`testCreateInterfaceWithTooManySockets` provoked a socket-creation failure by opening sockets
until the kernel refused. That was slow (locally 17s), and it exhausts a machine-wide
resource: while it runs, anything else needing a descriptor can fail, so it causes spurious
failures in unrelated tests and in other processes.

Lowering this process's own `RLIMIT_NOFILE` to what it is already using makes the very next
`socket()` fail, which is the condition the test wants, without affecting anything else, and
is fast (locally under a millisecond). The limit is restored before the assertion runs,
because XCTest opens files to report a failure. Reading and lowering the limit both stop the
test on failure rather than recording and continuing: a failed `getrlimit` leaves the value
zeroed, and lowering from that would clamp the hard limit to zero, which an unprivileged
process can never raise again.
@rnro rnro added the semver/none No version bump required. label Aug 21, 2026

func testEmptyPayload() {
let harness = UDPLoopbackHarness(basePort: 10950)
let harness = UDPLoopbackHarness()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice, thank you!

import Musl
internal import Logging
#elseif canImport(os)
internal import os

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just an empty #elseif branch now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver/none No version bump required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants