From 7c09026e51d9f731f3222b66242a9283abfd5ab1 Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Tue, 23 Jul 2024 21:47:23 +0200 Subject: [PATCH] add UniqueValues option to random.Slice to enable slice creation with unique values --- random/Make.go | 61 ++++++++++++++++++++++++++++++++++++++--- random/Make_test.go | 47 +++++++++++++++++++++++++++++++ random/Unique.go | 25 +++++++++++++---- random/Unique_test.go | 4 +-- random/examples_test.go | 20 +++++++++++--- 5 files changed, 142 insertions(+), 15 deletions(-) diff --git a/random/Make.go b/random/Make.go index d1aa822..08b72f9 100644 --- a/random/Make.go +++ b/random/Make.go @@ -4,21 +4,46 @@ func (r *Random) Make(T any) any { return r.Factory.Make(r, T) } -func Slice[T any](length int, mk func() T) []T { +func Slice[T any](length int, mk func() T, opts ...sliceOption) []T { + var c sliceConfig + c.use(opts) var vs []T for i := 0; i < length; i++ { - vs = append(vs, mk()) + var v T + if c.Unique { + v = Unique(mk, vs...) + } else { + v = mk() + } + vs = append(vs, v) } return vs } -func Map[K comparable, V any](length int, mk func() (K, V)) map[K]V { +func Map[K comparable, V any](length int, mk func() (K, V), opts ...mapOption) map[K]V { + var c mapConfig + c.use(opts) var ( vs = make(map[K]V) collisionRetries = 42 ) for i := 0; i < length; i++ { - k, v := mk() + var ( + k K + v V + ) + if c.Unique { + var vals []V + for _, val := range vs { + vals = append(vals, val) + } + Unique(func() V { + k, v = mk() + return v + }, vals...) + } else { + k, v = mk() + } if _, ok := vs[k]; ok { if 0 < collisionRetries { collisionRetries-- @@ -36,3 +61,31 @@ func KV[K comparable, V any](mkK func() K, mkV func() V) func() (K, V) { return mkK(), mkV() } } + +type sliceConfig struct { + Unique bool +} + +func (c *sliceConfig) use(opts []sliceOption) { + for _, opt := range opts { + opt.sliceOption(c) + } +} + +type sliceOption interface { + sliceOption(*sliceConfig) +} + +type mapConfig struct { + Unique bool +} + +func (c *mapConfig) use(opts []mapOption) { + for _, opt := range opts { + opt.mapOption(c) + } +} + +type mapOption interface { + mapOption(*mapConfig) +} diff --git a/random/Make_test.go b/random/Make_test.go index f0a0918..bb3cea9 100644 --- a/random/Make_test.go +++ b/random/Make_test.go @@ -305,6 +305,25 @@ func TestSlice_smoke(t *testing.T) { }) } +func TestSlice_withUniqueValues(t *testing.T) { + t.Run("without flag", func(t *testing.T) { + assert.Eventually(t, 10*time.Second, func(t assert.It) { + got := random.Slice[string](100, func() string { + return rnd.StringNC(3, random.CharsetDigit()) + }) + assert.NotUnique(t, got) + }) + }) + t.Run("with flag", func(t *testing.T) { + rnd.Repeat(3, 7, func() { + got := random.Slice[string](100, func() string { + return rnd.StringNC(3, random.CharsetDigit()) + }, random.UniqueValues) + assert.Unique(t, got) + }) + }) +} + func TestMap_smoke(t *testing.T) { it := assert.MakeIt(t) eventually := assert.MakeRetry(5 * time.Second) @@ -348,6 +367,34 @@ func TestMap_whenNotEnoughUniqueKeyCanBeGenerated_thenItReturnsWithLess(t *testi }) } +func TestMap_withUniqueValues(t *testing.T) { + values := func(m map[int]string) []string { + var vs []string + for _, v := range m { + vs = append(vs, v) + } + return vs + } + t.Run("without flag", func(t *testing.T) { + assert.Eventually(t, 10*time.Second, func(t assert.It) { + got := random.Map[int, string](100, func() (int, string) { + return rnd.Int(), rnd.StringNC(3, random.CharsetDigit()) + }) + + assert.NotUnique(t, values(got)) + }) + }) + t.Run("with flag", func(t *testing.T) { + rnd.Repeat(3, 7, func() { + got := random.Map[int, string](100, func() (int, string) { + return rnd.Int(), rnd.StringNC(3, random.CharsetDigit()) + }, random.UniqueValues) + + assert.Unique(t, values(got)) + }) + }) +} + func TestRandom_Make_structWithAnyField(t *testing.T) { type T struct { V1 any diff --git a/random/Unique.go b/random/Unique.go index 6623afd..6214056 100644 --- a/random/Unique.go +++ b/random/Unique.go @@ -30,12 +30,9 @@ func Unique[T any](blk func() T, excludeList ...T) T { ok bool = true ) for _, excluded := range excludeList { - isEqual, err := reflects.DeepEqual(v, excluded) - if err != nil { - panic(err.Error()) - } - if isEqual { + if eq(v, excluded) { ok = false + break } } if ok { @@ -44,3 +41,21 @@ func Unique[T any](blk func() T, excludeList ...T) T { } panic("random.Unique failed to find a unique value") } + +func eq[T any](a, b T) bool { + isEqual, err := reflects.DeepEqual(a, b) + if err != nil { + panic(err.Error()) + } + return isEqual +} + +// UniqueValues is an option that used to express a desire for unique value generation with certain functions. +// For example if random.Slice receives the UniqueValues flag, then the created values will be guaranteed to be unique, +// unless it is not possible within a reasonable attempts using the provided value maker function. +const UniqueValues = flagUniqueValues(0) + +type flagUniqueValues int + +func (flagUniqueValues) sliceOption(c *sliceConfig) { c.Unique = true } +func (flagUniqueValues) mapOption(c *mapConfig) { c.Unique = true } diff --git a/random/Unique_test.go b/random/Unique_test.go index 8fa4a2a..7626871 100644 --- a/random/Unique_test.go +++ b/random/Unique_test.go @@ -10,9 +10,10 @@ import ( "go.llib.dev/testcase/sandbox" ) +var rnd = random.New(random.CryptoSeed{}) + func ExampleUnique() { // useful when you need random values which are not equal - rnd := random.New(random.CryptoSeed{}) v1 := rnd.Int() v2 := random.Unique(rnd.Int, v1) v3 := random.Unique(rnd.Int, v1, v2) @@ -23,7 +24,6 @@ func ExampleUnique() { } func TestUnique(t *testing.T) { - rnd := random.New(random.CryptoSeed{}) t.Run("no exclude list given", func(t *testing.T) { v := random.Unique(rnd.Int) assert.NotEmpty(t, v) diff --git a/random/examples_test.go b/random/examples_test.go index 549b715..0bb16f7 100644 --- a/random/examples_test.go +++ b/random/examples_test.go @@ -155,16 +155,28 @@ func TestExampleRandomError(t *testing.T) { }) } -func ExampleMakeSlice() { +func ExampleSlice() { rnd := random.New(random.CryptoSeed{}) + slice := random.Slice[int](3, rnd.Int) + pp.PP(slice) // []int slice with 3 values +} - pp.PP(random.Slice[int](3, rnd.Int)) // []int slice with 3 values +func ExampleSlice_withUniqueValues() { + rnd := random.New(random.CryptoSeed{}) + slice := random.Slice[int](3, rnd.Int, random.UniqueValues) + pp.PP(slice) // []int slice with 3 values } -func ExampleMakeMap() { +func ExampleMap() { rnd := random.New(random.CryptoSeed{}) + m := random.Map[string, int](3, random.KV(rnd.String, rnd.Int)) + pp.PP(m) // map[string]int slice with 3 key-value pairs +} - pp.PP(random.Map[string, int](3, random.KV(rnd.String, rnd.Int))) // map[string]int slice with 3 key-value pairs +func ExampleMap_withUniqueValues() { + rnd := random.New(random.CryptoSeed{}) + m := random.Map[string, int](3, random.KV(rnd.String, rnd.Int), random.UniqueValues) + pp.PP(m) // map[string]int slice with 3 key-value pairs } func ExampleRandom_Repeat() {