All checks were successful
check / check (push) Successful in 1m18s
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
163 lines
2.9 KiB
Go
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",
|
|
)
|
|
}
|