Skip to content

Commit d760ab7

Browse files
TeoSlayerteovl
andauthored
test: cover handle table and embedded config (0.4% to 5.1%) (#1)
Add 18 internal tests for the only code paths reachable from a go test binary in this c-shared/main package: - handle table (storeHandle/loadHandle/deleteHandle): identity, uniqueness, idempotent delete, concurrent stress for -race - embeddedConfig.defaults(): blank fill, explicit preserve, negative/zero keepalive clamping, registry/beacon format The 45 //export wrappers plus errJSON/okJSON/driverFromHandle/ unmarshalCString carry C.* types in their signatures. Go's test toolchain rejects 'import "C"' in _test.go files for packages that already use //export (use of cgo in test … not supported), so those branches are only reachable from C-side integration tests against the compiled .so. Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
1 parent 9dc98f7 commit d760ab7

1 file changed

Lines changed: 309 additions & 0 deletions

File tree

zz_internal_test.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
package main
4+
5+
// This file deliberately avoids `import "C"`. Go's test toolchain
6+
// refuses to compile `_test.go` files that pull in cgo (see "use of
7+
// cgo in test … not supported"). Every helper exercised here uses
8+
// pure Go types in its signature, even though the package itself is a
9+
// c-shared cgo build. Anything whose signature carries a C.* type
10+
// (errJSON, okJSON, driverFromHandle, all //export wrappers,
11+
// unmarshalCString) can only be reached from C-side integration
12+
// tests.
13+
14+
import (
15+
"sync"
16+
"testing"
17+
)
18+
19+
// ---------------------------------------------------------------------
20+
// Handle table: storeHandle / loadHandle / deleteHandle
21+
//
22+
// These three power every PilotXxx wrapper's argument lookup. They are
23+
// also the only spot where the package holds onto Go heap objects that
24+
// the C side references by uint64 token — getting them wrong leaks
25+
// memory or hands out dangling handles. Worth testing exhaustively.
26+
// ---------------------------------------------------------------------
27+
28+
func TestStoreHandleAssignsUniqueMonotonicIDs(t *testing.T) {
29+
const N = 50
30+
ids := make([]uint64, N)
31+
for i := 0; i < N; i++ {
32+
ids[i] = storeHandle(i)
33+
}
34+
for i := 1; i < N; i++ {
35+
if ids[i] <= ids[i-1] {
36+
t.Fatalf("expected monotonic IDs; ids[%d]=%d ids[%d]=%d",
37+
i-1, ids[i-1], i, ids[i])
38+
}
39+
}
40+
for _, id := range ids {
41+
deleteHandle(id)
42+
}
43+
}
44+
45+
func TestStoreHandleNeverReturnsZero(t *testing.T) {
46+
// init() seeds next=1, so the very first store can never hand
47+
// out 0. Zero is reserved as "no handle" in the C ABI.
48+
id := storeHandle("first")
49+
defer deleteHandle(id)
50+
if id == 0 {
51+
t.Fatal("storeHandle returned the reserved zero id")
52+
}
53+
}
54+
55+
func TestStoreHandlePreservesIdentity(t *testing.T) {
56+
type marker struct{ name string }
57+
want := &marker{name: "round-trip"}
58+
id := storeHandle(want)
59+
defer deleteHandle(id)
60+
61+
got, ok := loadHandle(id)
62+
if !ok {
63+
t.Fatalf("loadHandle returned !ok for id %d", id)
64+
}
65+
gotMarker, ok := got.(*marker)
66+
if !ok {
67+
t.Fatalf("loaded value is %T, want *marker", got)
68+
}
69+
if gotMarker != want {
70+
t.Fatalf("identity mismatch: got %p want %p", gotMarker, want)
71+
}
72+
if gotMarker.name != "round-trip" {
73+
t.Fatalf("value mutated through table; got %q", gotMarker.name)
74+
}
75+
}
76+
77+
func TestStoreHandleAllowsNil(t *testing.T) {
78+
// Storing nil is legal — the table maps to interface{} and a
79+
// nil entry must still be retrievable (the C side may store
80+
// "no driver yet" sentinels).
81+
id := storeHandle(nil)
82+
defer deleteHandle(id)
83+
v, ok := loadHandle(id)
84+
if !ok {
85+
t.Fatal("loadHandle returned !ok for a nil-valued handle")
86+
}
87+
if v != nil {
88+
t.Fatalf("expected nil value; got %v", v)
89+
}
90+
}
91+
92+
func TestLoadHandleMissingReturnsFalse(t *testing.T) {
93+
if _, ok := loadHandle(0); ok {
94+
t.Fatal("loadHandle(0) reported ok; zero is never assigned")
95+
}
96+
if _, ok := loadHandle(1 << 60); ok {
97+
t.Fatal("loadHandle of a never-stored id should return false")
98+
}
99+
}
100+
101+
func TestDeleteHandleRemovesEntry(t *testing.T) {
102+
id := storeHandle("sentinel")
103+
if _, ok := loadHandle(id); !ok {
104+
t.Fatalf("precondition: handle %d should exist", id)
105+
}
106+
deleteHandle(id)
107+
if _, ok := loadHandle(id); ok {
108+
t.Fatalf("loadHandle(%d) still ok after deleteHandle", id)
109+
}
110+
}
111+
112+
func TestDeleteHandleIsIdempotent(t *testing.T) {
113+
// The C side is allowed to drop a handle twice (e.g. PilotClose
114+
// followed by an explicit FreeString during shutdown). The
115+
// second delete must be a silent no-op, not a panic.
116+
id := storeHandle("twice")
117+
deleteHandle(id)
118+
deleteHandle(id) // would panic if delete weren't idempotent
119+
}
120+
121+
func TestDeleteHandleUnknownIDDoesNotPanic(t *testing.T) {
122+
deleteHandle(1 << 62) // never assigned; must be a no-op
123+
}
124+
125+
func TestHandleTableMultipleConcurrentStores(t *testing.T) {
126+
// Race-detector hook: hammer the table from many goroutines and
127+
// confirm the sync.RWMutex pairing in store/load/delete holds up.
128+
// Run with `go test -race` to make this meaningful.
129+
const writers = 8
130+
const perWriter = 200
131+
var wg sync.WaitGroup
132+
wg.Add(writers)
133+
134+
collected := make(chan uint64, writers*perWriter)
135+
for w := 0; w < writers; w++ {
136+
go func(seed int) {
137+
defer wg.Done()
138+
for i := 0; i < perWriter; i++ {
139+
id := storeHandle(seed*1000 + i)
140+
v, ok := loadHandle(id)
141+
if !ok || v.(int) != seed*1000+i {
142+
t.Errorf("round-trip mismatch for id %d", id)
143+
return
144+
}
145+
collected <- id
146+
}
147+
}(w)
148+
}
149+
wg.Wait()
150+
close(collected)
151+
152+
seen := make(map[uint64]struct{}, writers*perWriter)
153+
for id := range collected {
154+
if _, dup := seen[id]; dup {
155+
t.Fatalf("duplicate id %d returned from storeHandle", id)
156+
}
157+
seen[id] = struct{}{}
158+
deleteHandle(id)
159+
}
160+
}
161+
162+
func TestHandleTableMixedStoreLoadDelete(t *testing.T) {
163+
// Interleave stores, loads, and deletes from independent
164+
// goroutines so the race detector can hunt for missing locks
165+
// on any of the three operations.
166+
const goroutines = 16
167+
const ops = 100
168+
169+
var wg sync.WaitGroup
170+
wg.Add(goroutines)
171+
for g := 0; g < goroutines; g++ {
172+
go func(seed int) {
173+
defer wg.Done()
174+
local := make([]uint64, 0, ops)
175+
for i := 0; i < ops; i++ {
176+
id := storeHandle(seed*ops + i)
177+
local = append(local, id)
178+
if v, ok := loadHandle(id); !ok || v.(int) != seed*ops+i {
179+
t.Errorf("g=%d round-trip mismatch id=%d", seed, id)
180+
return
181+
}
182+
}
183+
// Delete every handle we own; concurrent goroutines
184+
// must not see any cross-contamination.
185+
for _, id := range local {
186+
deleteHandle(id)
187+
}
188+
}(g)
189+
}
190+
wg.Wait()
191+
}
192+
193+
func TestHandleTableDistinctIDsForIdenticalValues(t *testing.T) {
194+
// Two stores of the same Go value must produce distinct
195+
// handles — the C side relies on token identity, not value
196+
// equality, to distinguish driver instances from each other.
197+
a := storeHandle("same-value")
198+
b := storeHandle("same-value")
199+
defer deleteHandle(a)
200+
defer deleteHandle(b)
201+
if a == b {
202+
t.Fatalf("storeHandle returned the same id %d for two stores", a)
203+
}
204+
}
205+
206+
// ---------------------------------------------------------------------
207+
// embeddedConfig.defaults()
208+
//
209+
// PilotEmbeddedStart relies on this to fill blanks before booting the
210+
// daemon; getting it wrong silently wires a different rendezvous than
211+
// the caller expects.
212+
// ---------------------------------------------------------------------
213+
214+
func TestEmbeddedConfigDefaultsFillsBlanks(t *testing.T) {
215+
var c embeddedConfig
216+
c.defaults()
217+
if c.RegistryAddr == "" {
218+
t.Fatal("RegistryAddr default not applied")
219+
}
220+
if c.BeaconAddr == "" {
221+
t.Fatal("BeaconAddr default not applied")
222+
}
223+
if c.KeepaliveSec <= 0 {
224+
t.Fatalf("KeepaliveSec default not applied: %d", c.KeepaliveSec)
225+
}
226+
if c.Version == "" {
227+
t.Fatal("Version default not applied")
228+
}
229+
}
230+
231+
func TestEmbeddedConfigDefaultsKeepsExplicitValues(t *testing.T) {
232+
c := embeddedConfig{
233+
RegistryAddr: "registry.example:9999",
234+
BeaconAddr: "beacon.example:8888",
235+
KeepaliveSec: 17,
236+
Version: "custom",
237+
}
238+
c.defaults()
239+
if c.RegistryAddr != "registry.example:9999" {
240+
t.Fatalf("registry addr overwritten: %s", c.RegistryAddr)
241+
}
242+
if c.BeaconAddr != "beacon.example:8888" {
243+
t.Fatalf("beacon addr overwritten: %s", c.BeaconAddr)
244+
}
245+
if c.KeepaliveSec != 17 {
246+
t.Fatalf("keepalive overwritten: %d", c.KeepaliveSec)
247+
}
248+
if c.Version != "custom" {
249+
t.Fatalf("version overwritten: %s", c.Version)
250+
}
251+
}
252+
253+
func TestEmbeddedConfigDefaultsNegativeKeepaliveClamped(t *testing.T) {
254+
// The guard is `<= 0`, so negatives must be clamped to the
255+
// default — otherwise daemon.Config would receive a negative
256+
// duration and silently skew keepalive timers.
257+
c := embeddedConfig{KeepaliveSec: -5}
258+
c.defaults()
259+
if c.KeepaliveSec <= 0 {
260+
t.Fatalf("expected positive keepalive after defaults; got %d", c.KeepaliveSec)
261+
}
262+
}
263+
264+
func TestEmbeddedConfigDefaultsZeroKeepaliveReplaced(t *testing.T) {
265+
c := embeddedConfig{KeepaliveSec: 0}
266+
c.defaults()
267+
if c.KeepaliveSec == 0 {
268+
t.Fatal("zero KeepaliveSec was not replaced with default")
269+
}
270+
}
271+
272+
func TestEmbeddedConfigDefaultsPreservesDataAndSocketPaths(t *testing.T) {
273+
// defaults() must never touch fields the caller cares about
274+
// for filesystem location.
275+
c := embeddedConfig{
276+
DataDir: "/var/lib/pilot",
277+
SocketPath: "/tmp/pilot.sock",
278+
}
279+
c.defaults()
280+
if c.DataDir != "/var/lib/pilot" {
281+
t.Fatalf("DataDir overwritten: %s", c.DataDir)
282+
}
283+
if c.SocketPath != "/tmp/pilot.sock" {
284+
t.Fatalf("SocketPath overwritten: %s", c.SocketPath)
285+
}
286+
}
287+
288+
func TestEmbeddedConfigDefaultsRegistryAddrFormat(t *testing.T) {
289+
// Sanity check: the default registry/beacon must be a
290+
// host:port-shaped string so daemon.New won't reject them. We
291+
// don't validate the host is reachable.
292+
var c embeddedConfig
293+
c.defaults()
294+
if !containsColon(c.RegistryAddr) {
295+
t.Fatalf("default RegistryAddr %q missing :port", c.RegistryAddr)
296+
}
297+
if !containsColon(c.BeaconAddr) {
298+
t.Fatalf("default BeaconAddr %q missing :port", c.BeaconAddr)
299+
}
300+
}
301+
302+
func containsColon(s string) bool {
303+
for i := 0; i < len(s); i++ {
304+
if s[i] == ':' {
305+
return true
306+
}
307+
}
308+
return false
309+
}

0 commit comments

Comments
 (0)