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  }