Files
dnswatcher/internal/resolver/livedns_harness_test.go
sneak 9cb2c2b7e0
All checks were successful
check / check (push) Successful in 1m18s
test: make live DNS tests robust instead of gated (closes #93)
The resolver's live-DNS tests failed nondeterministically, a different
subset each run. Three structural causes, all test-side:

- Burst fan-out. Every test in the package is parallel and the build
  hosts have many cores, so all ~35 iterative resolutions started at
  the same instant and, because queryServers walks rootServerList() in
  fixed order, hit the same root server within milliseconds. Root
  servers rate-limit that.
- No retry anywhere. One dropped UDP packet in a delegation chain
  failed a test outright.
- Unanimity assertions. TestQueryAllNameservers_AllReturnOK and
  _NXDomainFromAllNS required every one of a domain's nameservers to
  answer, with no tolerance for one being slow.

New internal/resolver/livedns_test.go addresses each: a package-wide
gate bounds how many live resolutions are in flight at once, every
live operation gets three attempts with exponential backoff and its
own deadline, and multi-nameserver assertions now need a strict
majority rather than unanimity. The retry predicate is deliberately
transport-level -- "did a nameserver answer at all" -- never the
assertion under test, so a resolver that answers incorrectly still
fails on the first attempt. A nameserver that stays silent is
tolerated; one that answers wrongly is not.

livedns_harness_test.go tests that machinery directly: quorum
arithmetic, status counting, the gate's concurrency bound, per-attempt
deadlines, and recovery from a transient failure. It touches no DNS.

Nothing is mocked, faked, stubbed, recorded, skipped or build-tagged,
and production resolver behaviour is unchanged.

Test caps move to the new org-wide values ruled at prompts issue 41:
60s hard cap, 20s target, 90s -timeout backstop. REPO_POLICIES.md is
re-vendored byte-identical from sneak/prompts rather than hand-edited,
which also picks up the golangci-lint paragraph this copy had drifted
behind on. TESTING.md's stale 30-second target follows to 60.

#93
2026-08-10 13:09:17 +00:00

163 lines
2.9 KiB
Go

package resolver_test
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"sneak.berlin/go/dnswatcher/internal/resolver"
)
// Tests for the live-DNS harness in livedns_test.go itself. These
// exercise pure logic and the retry/concurrency plumbing; they
// perform no DNS resolution of any kind, so they neither mock DNS
// nor depend on it.
func TestLiveQuorumIsStrictMajority(t *testing.T) {
t.Parallel()
cases := map[int]int{
0: 1,
1: 1,
2: 2,
3: 2,
4: 3,
5: 3,
13: 7,
}
for total, want := range cases {
assert.Equal(
t, want, liveQuorum(total),
"liveQuorum(%d)", total,
)
}
}
func TestStatusCountingIgnoresSilentNameservers(t *testing.T) {
t.Parallel()
results := map[string]*resolver.NameserverResponse{
"ns1.example.": {
Nameserver: "ns1.example.",
Status: resolver.StatusOK,
},
"ns2.example.": {
Nameserver: "ns2.example.",
Status: resolver.StatusOK,
},
"ns3.example.": {
Nameserver: "ns3.example.",
Status: resolver.StatusTimeout,
},
"ns4.example.": {
Nameserver: "ns4.example.",
Status: resolver.StatusError,
},
}
assert.Equal(
t, 2, countStatus(results, resolver.StatusOK),
)
assert.Equal(
t, 0, countStatus(results, resolver.StatusNXDomain),
)
// Two of four answered, which is short of the quorum of
// three: this is the state that triggers a retry rather
// than an assertion failure.
assert.Equal(t, 2, answeredCount(results))
assert.Less(t, answeredCount(results), liveQuorum(len(results)))
assert.Equal(
t,
"ns1.example.=ok ns2.example.=ok "+
"ns3.example.=timeout ns4.example.=error",
describeStatuses(results),
)
}
func TestRetryLiveRecoversFromTransientFailure(t *testing.T) {
t.Parallel()
const wantAttempts = 2
attempts := 0
retryLive(t, "transient", func(_ context.Context) error {
attempts++
if attempts < wantAttempts {
return errLiveNoAnswer
}
return nil
})
assert.Equal(t, wantAttempts, attempts)
}
func TestRetryLiveGivesEachAttemptADeadline(t *testing.T) {
t.Parallel()
retryLive(t, "deadline", func(ctx context.Context) error {
deadline, ok := ctx.Deadline()
assert.True(t, ok, "attempt should carry a deadline")
assert.LessOrEqual(
t, time.Until(deadline), liveAttemptTimeout,
)
return nil
})
}
func TestRunLiveBoundsConcurrency(t *testing.T) {
t.Parallel()
const workers = 24
var (
mu sync.Mutex
wg sync.WaitGroup
inFlight int
maxSeen int
)
wg.Add(workers)
for range workers {
go func() {
defer wg.Done()
_ = runLive(func(_ context.Context) error {
mu.Lock()
inFlight++
if inFlight > maxSeen {
maxSeen = inFlight
}
mu.Unlock()
time.Sleep(time.Millisecond)
mu.Lock()
inFlight--
mu.Unlock()
return nil
})
}()
}
wg.Wait()
assert.Positive(t, maxSeen)
assert.LessOrEqual(
t, maxSeen, liveConcurrency,
"live queries must stay under the package-wide gate",
)
}