github.com/mitranim/gg@v0.1.17/coll_test.go (about) 1 package gg_test 2 3 import ( 4 "testing" 5 6 "github.com/mitranim/gg" 7 "github.com/mitranim/gg/gtest" 8 ) 9 10 const testPanicStrEmptyKey = `unexpected empty key gg_test.SomeKey in gg_test.SomeModel` 11 12 type AnyColl interface{ SomeColl | SomeLazyColl } 13 14 /* 15 This is a workaround. Ideally we'd just use `AnyColl`, but Go wouldn't allow us 16 to access ANY of its methods or properties. 17 */ 18 type CollPtr[Coll AnyColl] interface { 19 *Coll 20 Len() int 21 IsEmpty() bool 22 IsNotEmpty() bool 23 Has(SomeKey) bool 24 Get(SomeKey) SomeModel 25 GetReq(SomeKey) SomeModel 26 Got(SomeKey) (SomeModel, bool) 27 Ptr(SomeKey) *SomeModel 28 PtrReq(SomeKey) *SomeModel 29 Add(...SomeModel) *Coll 30 Reset(...SomeModel) *Coll 31 Clear() *Coll 32 Reindex() *Coll 33 Swap(int, int) 34 MarshalJSON() ([]byte, error) 35 UnmarshalJSON([]byte) error 36 } 37 38 func TestColl(t *testing.T) { 39 defer gtest.Catch(t) 40 testColl[*SomeColl]() 41 } 42 43 func testColl[Ptr CollPtr[Coll], Coll AnyColl]() { 44 testCollReset[Ptr]() 45 testCollClear[Ptr]() 46 testCollHas[Ptr]() 47 testCollGet[Ptr]() 48 testCollGetReq[Ptr]() 49 testCollGot[Ptr]() 50 testCollPtr[Ptr]() 51 testCollPtrReq[Ptr]() 52 testColl_Len_IsEmpty_IsNotEmpty[Ptr]() 53 testCollAdd[Ptr]() 54 testCollReindex[Ptr]() 55 testCollSwap[Ptr]() 56 testCollMarshalJSON[Ptr]() 57 testCollUnmarshalJSON[Ptr]() 58 } 59 60 func testCollReset[Ptr CollPtr[Coll], Coll AnyColl]() { 61 var tar Coll 62 ptr := Ptr(&tar) 63 64 testEmpty := func() { 65 gtest.Eq(ptr, ptr.Reset()) 66 gtest.Zero(tar) 67 } 68 69 testEmpty() 70 71 test := func(src ...SomeModel) { 72 gtest.Eq(ptr, ptr.Reset(gg.Clone(src)...)) 73 testCollSlice(tar, src) 74 } 75 76 test() 77 test(SomeModel{10, `one`}) 78 test(SomeModel{10, `one`}, SomeModel{20, `two`}) 79 80 // Artifact of the implementation. Divergence from `Coll.Add` which would 81 // detect and deduplicate entries with the same primary key, although 82 // `LazyColl.Add` would not. This behavior exists in both `Coll` and 83 // `LazyColl` because `.Reset` is meant to store the slice as-is. 84 test(SomeModel{10, `one`}, SomeModel{10, `two`}, SomeModel{30, `three`}) 85 86 if gg.EqType[Coll, SomeColl]() { 87 // We're forced to exclude redundant elements from the index. 88 testCollIndex(tar, map[SomeKey]int{10: 1, 30: 2}) 89 } else if gg.EqType[Coll, SomeLazyColl]() { 90 testCollIndex(tar, nil) 91 } 92 93 testEmpty() 94 } 95 96 func testCollEqual[Coll AnyColl](src Coll, exp SomeColl) { 97 gtest.Equal(gg.Cast[SomeColl](src), exp) 98 } 99 100 /* 101 Workaround because Go doesn't let us access properties on values of type 102 parameters constrained by `IColl`. 103 */ 104 func testCollSlice[Coll AnyColl](src Coll, exp []SomeModel) { 105 gtest.Equal(getCollSlice(src), exp) 106 } 107 108 func getCollSlice[Coll AnyColl](src Coll) []SomeModel { 109 return gg.Cast[SomeColl](src).Slice 110 } 111 112 /* 113 Workaround because Go doesn't let us access properties on values of type 114 parameters constrained by `IColl`. 115 */ 116 func testCollIndex[Coll AnyColl](src Coll, exp map[SomeKey]int) { 117 gtest.Equal(getCollIndex(src), exp) 118 } 119 120 func getCollIndex[Coll SomeColl | SomeLazyColl](src Coll) map[SomeKey]int { 121 return gg.Cast[SomeColl](src).Index 122 } 123 124 func testCollClear[Ptr CollPtr[Coll], Coll AnyColl]() { 125 var tar Coll 126 ptr := Ptr(&tar) 127 128 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 129 gtest.NotZero(tar) 130 131 gtest.Eq(ptr, ptr.Clear()) 132 gtest.Zero(tar) 133 } 134 135 func testCollHas[Ptr CollPtr[Coll], Coll AnyColl]() { 136 var tar Coll 137 ptr := Ptr(&tar) 138 139 gtest.False(ptr.Has(0)) 140 gtest.False(ptr.Has(10)) 141 gtest.False(ptr.Has(20)) 142 gtest.False(ptr.Has(30)) 143 144 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 145 146 gtest.False(ptr.Has(0)) 147 gtest.True(ptr.Has(10)) 148 gtest.True(ptr.Has(20)) 149 gtest.False(ptr.Has(30)) 150 151 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.Has(0) }) 152 } 153 154 func testIdempotentIndexing(fun func(*SomeLazyColl)) { 155 var tar SomeLazyColl 156 tar.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 157 testCollIndex(tar, nil) 158 159 fun(&tar) 160 161 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 162 index := getCollIndex(tar) 163 164 fun(&tar) 165 166 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 167 168 // Technically, this doesn't guarantee that the collection doesn't rebuild the 169 // index by deleting and re-adding entries. We rely on this check because we 170 // know that we only clear the index by setting it to nil, and finding a 171 // different reference would indicate that it's been rebuilt. 172 gtest.Is(getCollIndex(tar), index, `must preserve existing index as-is`) 173 } 174 175 func testCollGet[Ptr CollPtr[Coll], Coll AnyColl]() { 176 var tar Coll 177 ptr := Ptr(&tar) 178 179 gtest.Zero(ptr.Get(0)) 180 gtest.Zero(ptr.Get(10)) 181 gtest.Zero(ptr.Get(20)) 182 gtest.Zero(ptr.Get(30)) 183 184 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 185 186 gtest.Zero(ptr.Get(0)) 187 gtest.Eq(ptr.Get(10), SomeModel{10, `one`}) 188 gtest.Eq(ptr.Get(20), SomeModel{20, `two`}) 189 gtest.Zero(ptr.Get(30)) 190 191 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.Get(0) }) 192 } 193 194 func testCollGetReq[Ptr CollPtr[Coll], Coll AnyColl]() { 195 var tar Coll 196 ptr := Ptr(&tar) 197 198 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 0`, func() { ptr.GetReq(0) }) 199 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 10`, func() { ptr.GetReq(10) }) 200 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 20`, func() { ptr.GetReq(20) }) 201 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 30`, func() { ptr.GetReq(30) }) 202 203 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 204 205 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 0`, func() { ptr.GetReq(0) }) 206 gtest.Eq(ptr.GetReq(10), SomeModel{10, `one`}) 207 gtest.Eq(ptr.GetReq(20), SomeModel{20, `two`}) 208 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 30`, func() { ptr.GetReq(30) }) 209 210 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.GetReq(10) }) 211 } 212 213 func testCollGot[Ptr CollPtr[Coll], Coll AnyColl]() { 214 var tar Coll 215 ptr := Ptr(&tar) 216 217 test := func(key SomeKey, expVal SomeModel, expOk bool) { 218 val, ok := ptr.Got(key) 219 gtest.Eq(expVal, val) 220 gtest.Eq(expOk, ok) 221 } 222 223 test(0, SomeModel{}, false) 224 test(10, SomeModel{}, false) 225 test(20, SomeModel{}, false) 226 test(30, SomeModel{}, false) 227 228 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 229 230 test(0, SomeModel{}, false) 231 test(10, SomeModel{10, `one`}, true) 232 test(20, SomeModel{20, `two`}, true) 233 test(30, SomeModel{}, false) 234 235 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.Got(0) }) 236 } 237 238 func testCollPtr[Ptr CollPtr[Coll], Coll AnyColl]() { 239 var tar Coll 240 ptr := Ptr(&tar) 241 242 gtest.Zero(ptr.Ptr(0)) 243 gtest.Zero(ptr.Ptr(10)) 244 gtest.Zero(ptr.Ptr(20)) 245 gtest.Zero(ptr.Ptr(30)) 246 247 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 248 249 gtest.Zero(ptr.Ptr(0)) 250 251 gtest.Equal(ptr.Ptr(10), &SomeModel{10, `one`}) 252 gtest.Eq(ptr.Ptr(10), ptr.Ptr(10)) 253 254 gtest.Equal(ptr.Ptr(20), &SomeModel{20, `two`}) 255 gtest.Eq(ptr.Ptr(20), ptr.Ptr(20)) 256 257 gtest.Zero(ptr.Ptr(30)) 258 259 ptr.Ptr(10).Name = `three` 260 gtest.Equal(ptr.Ptr(10), &SomeModel{10, `three`}) 261 262 ptr.Ptr(10).Name = `four` 263 gtest.Equal(ptr.Ptr(10), &SomeModel{10, `four`}) 264 265 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.Ptr(0) }) 266 } 267 268 func testCollPtrReq[Ptr CollPtr[Coll], Coll AnyColl]() { 269 var tar Coll 270 ptr := Ptr(&tar) 271 272 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 0`, func() { ptr.PtrReq(0) }) 273 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 10`, func() { ptr.PtrReq(10) }) 274 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 20`, func() { ptr.PtrReq(20) }) 275 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 30`, func() { ptr.PtrReq(30) }) 276 277 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 278 279 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 0`, func() { ptr.PtrReq(0) }) 280 gtest.Equal(ptr.PtrReq(10), &SomeModel{10, `one`}) 281 gtest.Equal(ptr.PtrReq(20), &SomeModel{20, `two`}) 282 gtest.PanicStr(`missing value of type gg_test.SomeModel for key 30`, func() { ptr.PtrReq(30) }) 283 284 ptr.PtrReq(10).Name = `three` 285 gtest.Equal(ptr.PtrReq(10), &SomeModel{10, `three`}) 286 287 ptr.PtrReq(10).Name = `four` 288 gtest.Equal(ptr.PtrReq(10), &SomeModel{10, `four`}) 289 290 testIdempotentIndexing(func(tar *SomeLazyColl) { tar.PtrReq(10) }) 291 } 292 293 func testColl_Len_IsEmpty_IsNotEmpty[Ptr CollPtr[Coll], Coll AnyColl]() { 294 var tar Coll 295 ptr := Ptr(&tar) 296 297 gtest.Eq(ptr.Len(), 0) 298 gtest.True(ptr.IsEmpty()) 299 gtest.False(ptr.IsNotEmpty()) 300 301 ptr.Reset(SomeModel{10, `one`}) 302 gtest.Eq(ptr.Len(), 1) 303 gtest.False(ptr.IsEmpty()) 304 gtest.True(ptr.IsNotEmpty()) 305 306 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 307 gtest.Eq(ptr.Len(), 2) 308 gtest.False(ptr.IsEmpty()) 309 gtest.True(ptr.IsNotEmpty()) 310 311 ptr.Clear() 312 gtest.Eq(ptr.Len(), 0) 313 gtest.True(ptr.IsEmpty()) 314 gtest.False(ptr.IsNotEmpty()) 315 } 316 317 func testCollAdd[Ptr CollPtr[Coll], Coll AnyColl]() { 318 var tar Coll 319 ptr := Ptr(&tar) 320 321 gtest.PanicStr(testPanicStrEmptyKey, func() { ptr.Add(SomeModel{}) }) 322 testCollSlice(tar, nil) 323 324 ptr.Add(SomeModel{10, `one`}) 325 testCollSlice(tar, []SomeModel{{10, `one`}}) 326 327 ptr.Add(SomeModel{20, `two`}) 328 testCollSlice(tar, []SomeModel{{10, `one`}, {20, `two`}}) 329 330 if gg.EqType[Coll, SomeColl]() { 331 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 332 } else if gg.EqType[Coll, SomeLazyColl]() { 333 testCollIndex(tar, nil) 334 } 335 336 // Known divergence between `Coll` and `LazyColl`. 337 { 338 var tar SomeColl 339 tar.Add(SomeModel{10, `one`}) 340 tar.Add(SomeModel{20, `two`}) 341 tar.Add(SomeModel{10, `three`}) 342 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 343 testCollSlice(tar, []SomeModel{{10, `three`}, {20, `two`}}) 344 } 345 // This happens when `LazyColl` is not indexed. 346 { 347 var tar SomeLazyColl 348 tar.Add(SomeModel{10, `one`}) 349 tar.Add(SomeModel{20, `two`}) 350 tar.Add(SomeModel{10, `three`}) 351 testCollIndex(tar, nil) 352 testCollSlice(tar, []SomeModel{{10, `one`}, {20, `two`}, {10, `three`}}) 353 } 354 // Reindexing (for any reason) avoids the problem. 355 { 356 var tar SomeLazyColl 357 tar.Add(SomeModel{10, `one`}) 358 tar.Add(SomeModel{20, `two`}) 359 testCollIndex(tar, nil) 360 tar.Reindex() 361 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 362 tar.Add(SomeModel{10, `three`}) 363 testCollSlice(tar, []SomeModel{{10, `three`}, {20, `two`}}) 364 } 365 } 366 367 func testCollReindex[Ptr CollPtr[Coll], Coll AnyColl]() { 368 var tar Coll 369 ptr := Ptr(&tar) 370 371 testCollIndex(tar, nil) 372 ptr.Reset(SomeModel{10, `one`}) 373 374 ptr.Reindex() 375 testCollIndex(tar, map[SomeKey]int{10: 0}) 376 } 377 378 func testCollSwap[Ptr CollPtr[Coll], Coll AnyColl]() { 379 var tar Coll 380 ptr := Ptr(&tar) 381 382 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}, SomeModel{30, `three`}) 383 prev := gg.CloneDeep(tar) 384 385 same := func(ind int) { 386 ptr.Swap(ind, ind) 387 gtest.Equal(prev, tar) 388 } 389 same(0) 390 same(1) 391 same(2) 392 testCollSlice(tar, []SomeModel{{10, `one`}, {20, `two`}, {30, `three`}}) 393 394 testGet := func() { 395 gtest.Equal(ptr.GetReq(10), SomeModel{10, `one`}) 396 gtest.Equal(ptr.GetReq(20), SomeModel{20, `two`}) 397 gtest.Equal(ptr.GetReq(30), SomeModel{30, `three`}) 398 } 399 400 ptr.Swap(0, 1) 401 testGet() 402 403 if gg.EqType[Coll, Coll]() { 404 testCollIndex(tar, map[SomeKey]int{10: 1, 20: 0, 30: 2}) 405 } else if gg.EqType[Coll, SomeLazyColl]() { 406 testCollIndex(tar, nil) 407 } 408 409 ptr.Swap(2, 0) 410 testGet() 411 412 if gg.EqType[Coll, Coll]() { 413 testCollIndex(tar, map[SomeKey]int{10: 1, 20: 2, 30: 0}) 414 } else if gg.EqType[Coll, SomeLazyColl]() { 415 testCollIndex(tar, nil) 416 } 417 } 418 419 func testCollMarshalJSON[Ptr CollPtr[Coll], Coll AnyColl]() { 420 var tar Coll 421 ptr := Ptr(&tar) 422 423 gtest.Eq(gg.JsonString(tar), `null`) 424 425 ptr.Reset(SomeModel{10, `one`}, SomeModel{20, `two`}) 426 427 gtest.Eq( 428 gg.JsonString(tar), 429 `[{"id":10,"name":"one"},{"id":20,"name":"two"}]`, 430 ) 431 } 432 433 func testCollUnmarshalJSON[Ptr CollPtr[Coll], Coll AnyColl]() { 434 tar := gg.JsonParseTo[Coll](`[ 435 {"id": 10, "name": "one"}, 436 {"id": 20, "name": "two"} 437 ]`) 438 439 testCollSlice(tar, []SomeModel{{10, `one`}, {20, `two`}}) 440 441 if gg.EqType[Coll, SomeColl]() { 442 testCollIndex(tar, map[SomeKey]int{10: 0, 20: 1}) 443 } else if gg.EqType[Coll, SomeLazyColl]() { 444 testCollIndex(tar, nil) 445 } 446 } 447 448 func TestCollOf(t *testing.T) { 449 defer gtest.Catch(t) 450 451 gtest.Zero(gg.CollOf[SomeKey, SomeModel]()) 452 453 testCollMake(func(src ...SomeModel) SomeColl { 454 return gg.CollOf[SomeKey, SomeModel](src...) 455 }) 456 } 457 458 func testCollMake[Coll AnyColl](fun func(...SomeModel) Coll) { 459 test := func(slice []SomeModel, index map[SomeKey]int) { 460 tar := fun(slice...) 461 testCollEqual(tar, SomeColl{Slice: slice, Index: index}) 462 gtest.Is(getCollSlice(tar), slice) 463 } 464 465 test( 466 []SomeModel{SomeModel{10, `one`}}, 467 map[SomeKey]int{10: 0}, 468 ) 469 470 test( 471 []SomeModel{ 472 SomeModel{10, `one`}, 473 SomeModel{20, `two`}, 474 }, 475 map[SomeKey]int{ 476 10: 0, 477 20: 1, 478 }, 479 ) 480 } 481 482 func TestCollFrom(t *testing.T) { 483 defer gtest.Catch(t) 484 485 gtest.Zero(gg.CollFrom[SomeKey, SomeModel, []SomeModel]()) 486 487 testCollMake(func(src ...SomeModel) SomeColl { 488 return gg.CollFrom[SomeKey, SomeModel](src) 489 }) 490 491 gtest.Equal( 492 gg.CollFrom[SomeKey, SomeModel]( 493 []SomeModel{ 494 SomeModel{10, `one`}, 495 SomeModel{20, `two`}, 496 }, 497 []SomeModel{ 498 SomeModel{30, `three`}, 499 SomeModel{40, `four`}, 500 }, 501 ), 502 SomeColl{ 503 Slice: []SomeModel{ 504 SomeModel{10, `one`}, 505 SomeModel{20, `two`}, 506 SomeModel{30, `three`}, 507 SomeModel{40, `four`}, 508 }, 509 Index: map[SomeKey]int{ 510 10: 0, 511 20: 1, 512 30: 2, 513 40: 3, 514 }, 515 }, 516 ) 517 }