Skip to content

Commit 974f781

Browse files
Add comprehensive world package tests and serialization
1 parent 550b40e commit 974f781

2 files changed

Lines changed: 291 additions & 24 deletions

File tree

world/coverage_test.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package world
2+
3+
import (
4+
"bytes"
5+
"encoding/gob"
6+
"sync"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestLocationInitValidation(t *testing.T) {
13+
t.Run("should validate inputs", func(t *testing.T) {
14+
loc := &Location{}
15+
16+
_, err := loc.init("ns", "", 0, 0)
17+
assert.ErrorIs(t, err, ErrLocationRequiredId)
18+
19+
_, err = loc.init("", "id", 0, 0)
20+
assert.ErrorIs(t, err, ErrLocationRequiredNamespace)
21+
22+
_, err = loc.init("ns", "id", 100, 0)
23+
assert.ErrorIs(t, err, ErrLocationInvalidLatitude)
24+
25+
_, err = loc.init("ns", "id", 0, 200)
26+
assert.ErrorIs(t, err, ErrLocationInvalidLongitude)
27+
28+
created, err := loc.init("ns", "id", 1, 1)
29+
assert.NoError(t, err)
30+
assert.Equal(t, "ns", created.Ns())
31+
assert.Equal(t, "id", created.Id())
32+
})
33+
}
34+
35+
func TestLocationString(t *testing.T) {
36+
loc, err := NewLocation("ns", "id", 1.5, 2.5)
37+
assert.NoError(t, err)
38+
assert.Equal(t, "ns,id,1.500000,2.500000", loc.String())
39+
}
40+
41+
func TestWorldSerialization(t *testing.T) {
42+
world := NewWorld()
43+
assert.NoError(t, world.Save("ns1", "loc1", 10, 20))
44+
assert.NoError(t, world.Save("ns2", "loc2", -10, -20))
45+
46+
serialized := world.ToBytes()
47+
assert.NotEmpty(t, serialized)
48+
49+
restored := NewWorldFromBytes(serialized)
50+
51+
loc1, ok := restored.GetLocation("ns1", "loc1")
52+
assert.True(t, ok)
53+
assert.Equal(t, 10.0, loc1.Lat())
54+
assert.Equal(t, 20.0, loc1.Lon())
55+
56+
loc2, ok := restored.GetLocation("ns2", "loc2")
57+
assert.True(t, ok)
58+
assert.Equal(t, -10.0, loc2.Lat())
59+
assert.Equal(t, -20.0, loc2.Lon())
60+
}
61+
62+
func TestWorldFromBytesInvalidPayload(t *testing.T) {
63+
assert.Panics(t, func() {
64+
_ = NewWorldFromBytes([]byte("not-a-gob"))
65+
})
66+
}
67+
68+
func TestWorldFromBytesSaveError(t *testing.T) {
69+
type serialLocation struct {
70+
ID string
71+
Lat float64
72+
Lon float64
73+
}
74+
75+
type serialNamespace struct {
76+
Name string
77+
Locations []serialLocation
78+
}
79+
80+
type serialWorld struct {
81+
Namespaces []serialNamespace
82+
}
83+
84+
invalid := serialWorld{Namespaces: []serialNamespace{{Name: "ns", Locations: []serialLocation{{ID: "bad", Lat: 200, Lon: 0}}}}}
85+
86+
var buf bytes.Buffer
87+
encoder := gob.NewEncoder(&buf)
88+
err := encoder.Encode(invalid)
89+
assert.NoError(t, err)
90+
91+
assert.Panics(t, func() {
92+
_ = NewWorldFromBytes(buf.Bytes())
93+
})
94+
}
95+
96+
func TestWorldNamespacePanics(t *testing.T) {
97+
world := &World{namespaces: map[string]*Namespace{"panicNS": nil}, mu: sync.RWMutex{}}
98+
99+
assert.PanicsWithValue(t, ErrUnexpectedNilNamespace, func() {
100+
world.Save("panicNS", "id", 0, 0)
101+
})
102+
103+
assert.PanicsWithValue(t, ErrUnexpectedNilNamespace, func() {
104+
world.Delete("panicNS", "id")
105+
})
106+
107+
assert.PanicsWithValue(t, ErrUnexpectedNilNamespace, func() {
108+
world.QueryRange("panicNS", 0, 1, 0, 1)
109+
})
110+
111+
assert.PanicsWithValue(t, ErrUnexpectedNilNamespace, func() {
112+
_, _ = world.GetLocation("panicNS", "id")
113+
})
114+
}
115+
116+
func TestWorldSaveValidationError(t *testing.T) {
117+
world := NewWorld()
118+
119+
err := world.Save("ns", "id", 200, 0)
120+
assert.ErrorIs(t, err, ErrLocationInvalidLatitude)
121+
}
122+
123+
func TestNamespaceUpdateError(t *testing.T) {
124+
ns := NewNamespace("ns")
125+
_, err := ns.SaveLocation("id", 1, 1)
126+
assert.NoError(t, err)
127+
128+
_, err = ns.SaveLocation("id", 200, 1)
129+
assert.ErrorIs(t, err, ErrLocationInvalidLatitude)
130+
}
131+
132+
func TestNamespaceInsertOutOfBounds(t *testing.T) {
133+
ns := &Namespace{Name: "ns", locations: map[string]*Location{}, tree: NewQuadTree(0, 1, 0, 1)}
134+
135+
_, err := ns.SaveLocation("id", 2, 2)
136+
assert.ErrorIs(t, err, ErrTreeLocationOutOfBounds)
137+
}
138+
139+
func TestMergeErrorPath(t *testing.T) {
140+
world1 := NewWorld()
141+
badWorld := NewWorld()
142+
143+
// Inject an invalid location directly to bypass validation and force a merge failure.
144+
badWorld.namespaces["ns"] = &Namespace{locations: map[string]*Location{"bad": {id: "bad", lat: 200, lon: 0, ns: "ns"}}, tree: NewQuadTree(-90, 90, -180, 180)}
145+
146+
assert.Panics(t, func() {
147+
world1.Merge(badWorld)
148+
})
149+
}
150+
151+
func TestTreeInsertOutOfBounds(t *testing.T) {
152+
node := NewTreeNode(0, 1, 0, 1, 1)
153+
loc, err := NewLocation("ns", "id", 2, 2)
154+
assert.NoError(t, err)
155+
156+
assert.ErrorIs(t, node.insert(loc), ErrTreeLocationOutOfBounds)
157+
}
158+
159+
func TestTreeDivideNoOpWhenAlreadyDivided(t *testing.T) {
160+
node := NewTreeNode(0, 1, 0, 1, 1)
161+
node.IsDivided = true
162+
163+
node.divide()
164+
165+
assert.True(t, node.IsDivided)
166+
}
167+
168+
func TestTreeQueryRangeNoOverlap(t *testing.T) {
169+
node := NewTreeNode(0, 1, 0, 1, 1)
170+
results := node.QueryRange(2, 3, 2, 3)
171+
172+
assert.Empty(t, results)
173+
}
174+
175+
func TestTreeDivideFallbacks(t *testing.T) {
176+
node := NewTreeNode(0, 10, 0, 10, 1)
177+
178+
locNW, err := NewLocation("ns", "nw", 1, 1)
179+
assert.NoError(t, err)
180+
assert.NoError(t, node.insert(locNW))
181+
182+
locSE, err := NewLocation("ns", "se", 1, 9)
183+
assert.NoError(t, err)
184+
assert.NoError(t, node.insert(locSE))
185+
186+
assert.True(t, node.IsDivided)
187+
assert.Equal(t, node.SE, locSE.Node)
188+
}
189+
190+
func TestTreeInsertRelocatesExistingNode(t *testing.T) {
191+
tree := NewQuadTree(-90, 90, -180, 180)
192+
193+
loc, err := NewLocation("ns", "id", -80, -170)
194+
assert.NoError(t, err)
195+
assert.NoError(t, tree.Insert(loc))
196+
197+
err = loc.Update(80, 170)
198+
assert.NoError(t, err)
199+
200+
assert.NoError(t, tree.Insert(loc))
201+
assert.NotNil(t, loc.Node)
202+
assert.True(t, loc.Node.Lat1 >= 0)
203+
}
204+
205+
func TestTreeDivideWithNilLocationPanics(t *testing.T) {
206+
node := NewTreeNode(0, 10, 0, 10, 1)
207+
node.Objects["nil"] = nil
208+
209+
assert.Panics(t, func() {
210+
node.divide()
211+
})
212+
}
213+
214+
func TestTreeInsertRemovesFromPreviousNode(t *testing.T) {
215+
node := NewTreeNode(0, 1, 0, 1, 2)
216+
previous := NewTreeNode(0, 1, 0, 1, 2)
217+
218+
loc, err := NewLocation("ns", "reassign", 0.5, 0.5)
219+
assert.NoError(t, err)
220+
221+
loc.SetNode(previous)
222+
previous.Objects[loc.Id()] = loc
223+
224+
assert.NoError(t, node.insert(loc))
225+
_, exists := previous.Objects[loc.Id()]
226+
assert.False(t, exists)
227+
}
228+
229+
func TestTreeInsertNilLocation(t *testing.T) {
230+
node := NewTreeNode(0, 1, 0, 1, 1)
231+
232+
assert.ErrorIs(t, node.insert(nil), ErrTreeLocationNil)
233+
}

world/world.go

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,13 @@ func NewWorld() *World {
3535

3636
func (m *World) Delete(ns, locId string) {
3737
namespace := m.getNamespace(ns)
38-
if namespace == nil {
39-
panic(ErrUnexpectedNilNamespace)
40-
}
41-
4238
namespace.DeleteLocation(locId)
4339
}
4440

4541
// Save a location to the world. If the location already exists, it will be updated.
4642
func (m *World) Save(ns, locId string, lat, lon float64) error {
4743
namespace := m.getNamespace(ns)
4844

49-
if namespace == nil {
50-
panic(ErrUnexpectedNilNamespace)
51-
}
52-
5345
_, err := namespace.SaveLocation(locId, lat, lon)
5446

5547
return err
@@ -76,30 +68,80 @@ func (m *World) getNamespace(ns string) *Namespace {
7668
}
7769

7870
func (m *World) ToBytes() []byte {
79-
var buf bytes.Buffer
71+
type serialLocation struct {
72+
ID string
73+
Lat float64
74+
Lon float64
75+
}
8076

81-
enc := gob.NewEncoder(&buf)
82-
err := enc.Encode(m)
77+
type serialNamespace struct {
78+
Name string
79+
Locations []serialLocation
80+
}
8381

84-
if err != nil {
82+
type serialWorld struct {
83+
Namespaces []serialNamespace
84+
}
85+
86+
m.mu.RLock()
87+
dto := serialWorld{}
88+
89+
for _, namespace := range m.namespaces {
90+
namespace.mu.RLock()
91+
nsDTO := serialNamespace{Name: namespace.Name}
92+
for id, loc := range namespace.locations {
93+
nsDTO.Locations = append(nsDTO.Locations, serialLocation{ID: id, Lat: loc.Lat(), Lon: loc.Lon()})
94+
}
95+
namespace.mu.RUnlock()
8596

86-
return []byte{}
97+
dto.Namespaces = append(dto.Namespaces, nsDTO)
8798
}
99+
m.mu.RUnlock()
100+
101+
var buf bytes.Buffer
102+
103+
_ = gob.NewEncoder(&buf).Encode(dto)
88104

89105
return buf.Bytes()
90106
}
91107

92108
func NewWorldFromBytes(buf []byte) *World {
93-
var w World
109+
type serialLocation struct {
110+
ID string
111+
Lat float64
112+
Lon float64
113+
}
114+
115+
type serialNamespace struct {
116+
Name string
117+
Locations []serialLocation
118+
}
119+
120+
type serialWorld struct {
121+
Namespaces []serialNamespace
122+
}
123+
124+
var dto serialWorld
94125

95126
dec := gob.NewDecoder(bytes.NewReader(buf))
96-
err := dec.Decode(&w)
127+
err := dec.Decode(&dto)
97128

98129
if err != nil {
99130
panic(err)
100131
}
101132

102-
return &w
133+
world := NewWorld()
134+
135+
for _, namespace := range dto.Namespaces {
136+
for _, loc := range namespace.Locations {
137+
err := world.Save(namespace.Name, loc.ID, loc.Lat, loc.Lon)
138+
if err != nil {
139+
panic(err)
140+
}
141+
}
142+
}
143+
144+
return world
103145
}
104146

105147
func (m *World) Merge(w *World) {
@@ -119,10 +161,6 @@ func (m *World) Merge(w *World) {
119161
func (m *World) GetLocation(ns, id string) (Location, bool) {
120162
namespace := m.getNamespace(ns)
121163

122-
if namespace == nil {
123-
panic(ErrUnexpectedNilNamespace)
124-
}
125-
126164
location, ok := namespace.GetLocation(id)
127165
if !ok {
128166
return Location{}, false
@@ -134,9 +172,5 @@ func (m *World) GetLocation(ns, id string) (Location, bool) {
134172
func (m *World) QueryRange(ns string, lat1, lat2, lon1, lon2 float64) []*Location {
135173
namespace := m.getNamespace(ns)
136174

137-
if namespace == nil {
138-
panic(ErrUnexpectedNilNamespace)
139-
}
140-
141175
return namespace.QueryRange(lat1, lat2, lon1, lon2)
142176
}

0 commit comments

Comments
 (0)