• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mlange-42 / ark / 13642217050

03 Mar 2025 11:02PM CUT coverage: 99.261% (+0.4%) from 98.883%
13642217050

push

github

web-flow
Add more unit tests (#119)

* test entity pool capacity
* test relations panic
* test table match panics
* test register on locked world
* test cache panic
* test storage panics
* test exchange mask panics

3 of 3 new or added lines in 1 file covered. (100.0%)

5772 of 5815 relevant lines covered (99.26%)

34225.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.18
/ecs/storage.go
1
package ecs
2

3
import (
4
        "fmt"
5
        "unsafe"
6
)
7

8
type storage struct {
9
        config     config
10
        registry   componentRegistry
11
        entities   []entityIndex
12
        isTarget   []bool
13
        entityPool entityPool
14
        graph      graph
15
        cache      cache
16

17
        archetypes         []archetype
18
        relationArchetypes []archetypeID
19
        tables             []table
20
        components         []componentStorage
21
}
22

23
type componentStorage struct {
24
        columns []*column
25
}
26

27
func newStorage(capacity ...int) storage {
193✔
28
        config := newConfig(capacity...)
193✔
29

193✔
30
        reg := newComponentRegistry()
193✔
31
        entities := make([]entityIndex, reservedEntities, config.initialCapacity+reservedEntities)
193✔
32
        isTarget := make([]bool, reservedEntities, config.initialCapacity+reservedEntities)
193✔
33
        // Reserved zero and wildcard entities
193✔
34
        for i := range reservedEntities {
579✔
35
                entities[i] = entityIndex{table: maxTableID, row: 0}
386✔
36
        }
386✔
37
        componentsMap := make([]int16, MaskTotalBits)
193✔
38
        for i := range MaskTotalBits {
49,601✔
39
                componentsMap[i] = -1
49,408✔
40
        }
49,408✔
41

42
        tables := make([]table, 0, 128)
193✔
43
        tables = append(tables, newTable(0, 0, uint32(config.initialCapacity), &reg, []ID{}, componentsMap, []bool{}, []Entity{}, []RelationID{}))
193✔
44
        archetypes := make([]archetype, 0, 128)
193✔
45
        archetypes = append(archetypes, newArchetype(0, 0, &Mask{}, []ID{}, []tableID{0}, &reg))
193✔
46
        return storage{
193✔
47
                config:     config,
193✔
48
                registry:   reg,
193✔
49
                cache:      newCache(),
193✔
50
                entities:   entities,
193✔
51
                isTarget:   isTarget,
193✔
52
                entityPool: newEntityPool(uint32(config.initialCapacity), reservedEntities),
193✔
53
                graph:      newGraph(),
193✔
54
                archetypes: archetypes,
193✔
55
                tables:     tables,
193✔
56
                components: make([]componentStorage, 0, MaskTotalBits),
193✔
57
        }
193✔
58
}
59

60
func (s *storage) findOrCreateTable(oldTable *table, add []ID, remove []ID, relations []RelationID) *table {
496,994✔
61
        startNode := s.archetypes[oldTable.archetype].node
496,994✔
62

496,994✔
63
        node := s.graph.Find(startNode, add, remove)
496,994✔
64
        var arch *archetype
496,994✔
65
        if archID, ok := node.GetArchetype(); ok {
993,596✔
66
                arch = &s.archetypes[archID]
496,602✔
67
        } else {
496,994✔
68
                arch = s.createArchetype(node)
392✔
69
                node.archetype = arch.id
392✔
70
        }
392✔
71

72
        allRelation := appendNew(oldTable.relationIDs, relations...)
496,994✔
73
        table, ok := arch.GetTable(s, allRelation)
496,994✔
74
        if !ok {
497,440✔
75
                table = s.createTable(arch, allRelation)
446✔
76
        }
446✔
77
        return table
496,994✔
78
}
79

80
func (s *storage) AddComponent(id uint8) {
888✔
81
        if len(s.components) != int(id) {
889✔
82
                panic("components can only be added to a storage sequentially")
1✔
83
        }
84
        s.components = append(s.components, componentStorage{columns: make([]*column, len(s.tables))})
887✔
85
}
86

87
// RemoveEntity removes the given entity from the world.
88
func (s *storage) RemoveEntity(entity Entity) {
494,702✔
89
        if !s.entityPool.Alive(entity) {
494,703✔
90
                panic("can't remove a dead entity")
1✔
91
        }
92
        index := &s.entities[entity.id]
494,701✔
93
        table := &s.tables[index.table]
494,701✔
94

494,701✔
95
        swapped := table.Remove(index.row)
494,701✔
96

494,701✔
97
        s.entityPool.Recycle(entity)
494,701✔
98

494,701✔
99
        if swapped {
988,206✔
100
                swapEntity := table.GetEntity(uintptr(index.row))
493,505✔
101
                s.entities[swapEntity.id].row = index.row
493,505✔
102
        }
493,505✔
103
        index.table = maxTableID
494,701✔
104

494,701✔
105
        if s.isTarget[entity.id] {
494,704✔
106
                s.cleanupArchetypes(entity)
3✔
107
                s.isTarget[entity.id] = false
3✔
108
        }
3✔
109
}
110

111
func (s *storage) Reset() {
1✔
112
        s.entities = s.entities[:reservedEntities]
1✔
113
        s.entityPool.Reset()
1✔
114
        s.isTarget = s.isTarget[:reservedEntities]
1✔
115

1✔
116
        for i := range s.archetypes {
5✔
117
                s.archetypes[i].Reset(s)
4✔
118
        }
4✔
119
}
120

121
func (s *storage) get(entity Entity, component ID) unsafe.Pointer {
5✔
122
        if !s.entityPool.Alive(entity) {
6✔
123
                panic("can't get component of a dead entity")
1✔
124
        }
125
        return s.getUnchecked(entity, component)
4✔
126
}
127

128
func (s *storage) getUnchecked(entity Entity, component ID) unsafe.Pointer {
5✔
129
        s.checkHasComponent(entity, component)
5✔
130
        index := s.entities[entity.id]
5✔
131
        return s.tables[index.table].Get(component, uintptr(index.row))
5✔
132
}
5✔
133

134
func (s *storage) has(entity Entity, component ID) bool {
17✔
135
        if !s.entityPool.Alive(entity) {
18✔
136
                panic("can't get component of a dead entity")
1✔
137
        }
138
        return s.hasUnchecked(entity, component)
16✔
139
}
140

141
func (s *storage) hasUnchecked(entity Entity, component ID) bool {
18✔
142
        s.checkHasComponent(entity, component)
18✔
143
        index := s.entities[entity.id]
18✔
144
        return s.tables[index.table].Has(component)
18✔
145
}
18✔
146

147
func (s *storage) getRelation(entity Entity, comp ID) Entity {
346✔
148
        if !s.entityPool.Alive(entity) {
347✔
149
                panic("can't get relation for a dead entity")
1✔
150
        }
151
        return s.getRelationUnchecked(entity, comp)
345✔
152
}
153

154
func (s *storage) getRelationUnchecked(entity Entity, comp ID) Entity {
349✔
155
        s.checkHasComponent(entity, comp)
349✔
156
        return s.tables[s.entities[entity.id].table].GetRelation(comp)
349✔
157
}
349✔
158

159
func (s *storage) registerTargets(relations []RelationID) {
497,046✔
160
        for _, rel := range relations {
497,796✔
161
                s.isTarget[rel.target.id] = true
750✔
162
        }
750✔
163
}
164

165
func (s *storage) registerFilter(batch *Batch) cacheID {
30✔
166
        return s.cache.register(s, batch)
30✔
167
}
30✔
168

169
func (s *storage) unregisterFilter(entry cacheID) {
28✔
170
        s.cache.unregister(entry)
28✔
171
}
28✔
172

173
func (s *storage) getRegisteredFilter(id cacheID) *cacheEntry {
28✔
174
        return s.cache.getEntry(id)
28✔
175
}
28✔
176

177
func (s *storage) createEntity(table tableID) (Entity, uint32) {
496,450✔
178
        entity := s.entityPool.Get()
496,450✔
179

496,450✔
180
        idx := s.tables[table].Add(entity)
496,450✔
181
        len := len(s.entities)
496,450✔
182
        if int(entity.id) == len {
501,643✔
183
                s.entities = append(s.entities, entityIndex{table: table, row: idx})
5,193✔
184
                s.isTarget = append(s.isTarget, false)
5,193✔
185
        } else {
496,450✔
186
                s.entities[entity.id] = entityIndex{table: table, row: idx}
491,257✔
187
        }
491,257✔
188
        return entity, idx
496,450✔
189
}
190

191
func (s *storage) createEntities(table *table, count int) {
142✔
192
        startIdx := table.Len()
142✔
193
        table.Alloc(uint32(count))
142✔
194

142✔
195
        len := len(s.entities)
142✔
196
        for i := range count {
2,196✔
197
                index := uint32(startIdx + i)
2,054✔
198
                entity := s.entityPool.Get()
2,054✔
199
                table.SetEntity(index, entity)
2,054✔
200

2,054✔
201
                if int(entity.id) == len {
4,106✔
202
                        s.entities = append(s.entities, entityIndex{table: table.id, row: index})
2,052✔
203
                        s.isTarget = append(s.isTarget, false)
2,052✔
204
                        len++
2,052✔
205
                } else {
2,054✔
206
                        s.entities[entity.id] = entityIndex{table: table.id, row: index}
2✔
207
                }
2✔
208
        }
209
}
210

211
func (s *storage) createArchetype(node *node) *archetype {
392✔
212
        comps := node.mask.toTypes(&s.registry.registry)
392✔
213
        index := len(s.archetypes)
392✔
214
        s.archetypes = append(s.archetypes, newArchetype(archetypeID(index), node.id, &node.mask, comps, nil, &s.registry))
392✔
215
        archetype := &s.archetypes[index]
392✔
216
        if archetype.HasRelations() {
440✔
217
                s.relationArchetypes = append(s.relationArchetypes, archetype.id)
48✔
218
        }
48✔
219
        return archetype
392✔
220
}
221

222
func (s *storage) createTable(archetype *archetype, relations []RelationID) *table {
468✔
223
        targets := make([]Entity, len(archetype.components))
468✔
224
        numRelations := uint8(0)
468✔
225
        for _, rel := range relations {
600✔
226
                idx := archetype.componentsMap[rel.component.id]
132✔
227
                targets[idx] = rel.target
132✔
228
                numRelations++
132✔
229
        }
132✔
230
        if numRelations != archetype.numRelations {
468✔
231
                panic("relations must be fully specified")
×
232
        }
233

234
        var newTableID tableID
468✔
235
        if id, ok := archetype.GetFreeTable(); ok {
470✔
236
                newTableID = id
2✔
237
                s.tables[newTableID].recycle(targets, relations)
2✔
238
        } else {
468✔
239
                newTableID = tableID(len(s.tables))
466✔
240
                cap := s.config.initialCapacity
466✔
241
                if archetype.HasRelations() {
588✔
242
                        cap = s.config.initialCapacityRelations
122✔
243
                }
122✔
244
                s.tables = append(s.tables, newTable(
466✔
245
                        newTableID, archetype.id, uint32(cap), &s.registry,
466✔
246
                        archetype.components, archetype.componentsMap,
466✔
247
                        archetype.isRelation, targets, relations))
466✔
248
        }
249
        archetype.AddTable(&s.tables[newTableID])
468✔
250

468✔
251
        table := &s.tables[newTableID]
468✔
252
        for i := range s.components {
2,994✔
253
                id := ID{id: uint8(i)}
2,526✔
254
                comps := &s.components[i]
2,526✔
255
                if archetype.mask.Get(id) {
4,241✔
256
                        comps.columns = append(comps.columns, table.GetColumn(id))
1,715✔
257
                } else {
2,526✔
258
                        comps.columns = append(comps.columns, nil)
811✔
259
                }
811✔
260
        }
261

262
        s.cache.addTable(s, table)
468✔
263
        return table
468✔
264
}
265

266
func (s *storage) getExchangeMask(mask *Mask, add []ID, rem []ID) {
809✔
267
        for _, comp := range rem {
2,009✔
268
                if !mask.Get(comp) {
1,201✔
269
                        panic(fmt.Sprintf("entity does not have a component of type %v, can't remove", s.registry.Types[comp.id]))
1✔
270
                }
271
                mask.Set(comp, false)
1,199✔
272
        }
273
        for _, comp := range add {
1,990✔
274
                if mask.Get(comp) {
1,183✔
275
                        panic(fmt.Sprintf("entity already has component of type %v, can't add", s.registry.Types[comp.id]))
1✔
276
                }
277
                mask.Set(comp, true)
1,181✔
278
        }
279
}
280

281
// Removes empty archetypes that have a target relation to the given entity.
282
func (s *storage) cleanupArchetypes(target Entity) {
6✔
283
        newRelations := []RelationID{}
6✔
284
        for _, arch := range s.relationArchetypes {
19✔
285
                archetype := &s.archetypes[arch]
13✔
286
                len := len(archetype.tables)
13✔
287
                for i := len - 1; i >= 0; i-- {
33✔
288
                        table := &s.tables[archetype.tables[i]]
20✔
289

20✔
290
                        foundTarget := false
20✔
291
                        for _, rel := range table.relationIDs {
40✔
292
                                if rel.target.id == target.id {
29✔
293
                                        newRelations = append(newRelations, RelationID{component: rel.component, target: Entity{}})
9✔
294
                                        foundTarget = true
9✔
295
                                }
9✔
296
                        }
297
                        if !foundTarget {
31✔
298
                                continue
11✔
299
                        }
300

301
                        if table.Len() > 0 {
10✔
302
                                allRelations := s.getExchangeTargetsUnchecked(table, newRelations)
1✔
303
                                newTable, ok := archetype.GetTable(s, allRelations)
1✔
304
                                if !ok {
2✔
305
                                        newTable = s.createTable(archetype, newRelations)
1✔
306
                                }
1✔
307
                                s.moveEntities(table, newTable, uint32(table.Len()))
1✔
308
                        }
309
                        archetype.FreeTable(table.id)
9✔
310
                        s.cache.removeTable(s, table)
9✔
311

9✔
312
                        newRelations = newRelations[:0]
9✔
313
                }
314
                archetype.RemoveTarget(target)
13✔
315
        }
316
}
317

318
// moveEntities moves all entities from src to dst.
319
func (s *storage) moveEntities(src, dst *table, count uint32) {
10✔
320
        oldLen := dst.Len()
10✔
321
        dst.AddAll(src, count)
10✔
322

10✔
323
        newLen := dst.Len()
10✔
324
        newTable := dst.id
10✔
325
        for i := oldLen; i < newLen; i++ {
258✔
326
                entity := dst.GetEntity(uintptr(i))
248✔
327
                s.entities[entity.id] = entityIndex{table: newTable, row: uint32(i)}
248✔
328
        }
248✔
329
        src.Reset()
10✔
330
}
331

332
func (s *storage) getExchangeTargetsUnchecked(oldTable *table, relations []RelationID) []RelationID {
1✔
333
        targets := make([]Entity, len(oldTable.columns))
1✔
334
        for i := range oldTable.columns {
3✔
335
                targets[i] = oldTable.columns[i].target
2✔
336
        }
2✔
337
        for _, rel := range relations {
2✔
338
                index := oldTable.components[rel.component.id]
1✔
339
                if rel.target == targets[index] {
1✔
340
                        continue
×
341
                }
342
                targets[index] = rel.target
1✔
343
        }
344

345
        result := make([]RelationID, 0, len(oldTable.relationIDs))
1✔
346
        for i, e := range targets {
3✔
347
                if !oldTable.columns[i].isRelation {
3✔
348
                        continue
1✔
349
                }
350
                id := oldTable.ids[i]
1✔
351
                result = append(result, RelationID{component: id, target: e})
1✔
352
        }
353

354
        return result
1✔
355
}
356

357
func (s *storage) getExchangeTargets(oldTable *table, relations []RelationID) ([]RelationID, bool) {
52✔
358
        changed := false
52✔
359
        targets := make([]Entity, len(oldTable.columns))
52✔
360
        for i := range oldTable.columns {
197✔
361
                targets[i] = oldTable.columns[i].target
145✔
362
        }
145✔
363
        for _, rel := range relations {
105✔
364
                if !rel.target.IsZero() && !s.entityPool.Alive(rel.target) {
53✔
365
                        panic("can't make a dead entity a relation target")
×
366
                }
367
                index := oldTable.components[rel.component.id]
53✔
368
                if !oldTable.columns[index].isRelation {
53✔
369
                        panic(fmt.Sprintf("component %d is not a relation component", rel.component.id))
×
370
                }
371
                if rel.target == targets[index] {
53✔
372
                        continue
×
373
                }
374
                targets[index] = rel.target
53✔
375
                changed = true
53✔
376
        }
377
        if !changed {
52✔
378
                return nil, false
×
379
        }
×
380

381
        result := make([]RelationID, 0, len(oldTable.relationIDs))
52✔
382
        for i, e := range targets {
197✔
383
                if !oldTable.columns[i].isRelation {
236✔
384
                        continue
91✔
385
                }
386
                id := oldTable.ids[i]
54✔
387
                result = append(result, RelationID{component: id, target: e})
54✔
388
        }
389

390
        return result, true
52✔
391
}
392

393
func (s *storage) getTables(batch *Batch) []*table {
91✔
394
        tables := []*table{}
91✔
395

91✔
396
        for i := range s.archetypes {
422✔
397
                archetype := &s.archetypes[i]
331✔
398
                if !batch.filter.matches(&archetype.mask) {
486✔
399
                        continue
155✔
400
                }
401

402
                if !archetype.HasRelations() {
340✔
403
                        table := &s.tables[archetype.tables[0]]
164✔
404
                        tables = append(tables, table)
164✔
405
                        continue
164✔
406
                }
407

408
                tableIDs := archetype.GetTables(batch.relations)
12✔
409
                for _, tab := range tableIDs {
27✔
410
                        table := &s.tables[tab]
15✔
411
                        if !table.Matches(batch.relations) {
15✔
412
                                continue
×
413
                        }
414
                        tables = append(tables, table)
15✔
415
                }
416
        }
417
        return tables
91✔
418
}
419

420
func (s *storage) getTableIDs(batch *Batch) []tableID {
32✔
421
        tables := []tableID{}
32✔
422

32✔
423
        for i := range s.archetypes {
97✔
424
                archetype := &s.archetypes[i]
65✔
425
                if !batch.filter.matches(&archetype.mask) {
98✔
426
                        continue
33✔
427
                }
428

429
                if !archetype.HasRelations() {
39✔
430
                        tables = append(tables, archetype.tables[0])
7✔
431
                        continue
7✔
432
                }
433

434
                tableIDs := archetype.GetTables(batch.relations)
25✔
435
                for _, tab := range tableIDs {
84✔
436
                        table := &s.tables[tab]
59✔
437
                        if !table.Matches(batch.relations) {
59✔
438
                                continue
×
439
                        }
440
                        tables = append(tables, tab)
59✔
441
                }
442
        }
443
        return tables
32✔
444
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc