Skip to content

Commit

Permalink
Support array generators (addresses #86)
Browse files Browse the repository at this point in the history
  • Loading branch information
untoldwind committed Apr 2, 2024
1 parent f9f2f29 commit 69954c9
Show file tree
Hide file tree
Showing 16 changed files with 910 additions and 72 deletions.
24 changes: 12 additions & 12 deletions arbitrary/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ automatically combine generators as needed using reflection.
A simple example might look like this:
func TestIntParse(t *testing.T) {
properties := gopter.NewProperties(nil)
arbitraries := arbitrary.DefaultArbitraries()
func TestIntParse(t *testing.T) {
properties := gopter.NewProperties(nil)
arbitraries := arbitrary.DefaultArbitraries()
properties.Property("printed integers can be parsed", arbitraries.ForAll(
func(a int64) bool {
str := fmt.Sprintf("%d", a)
parsed, err := strconv.ParseInt(str, 10, 64)
return err == nil && parsed == a
}))
properties.Property("printed integers can be parsed", arbitraries.ForAll(
func(a int64) bool {
str := fmt.Sprintf("%d", a)
parsed, err := strconv.ParseInt(str, 10, 64)
return err == nil && parsed == a
}))
properties.TestingRun(t)
}
properties.TestingRun(t)
}
Be aware that by default always the most generic generators are used. I.e. in
the example above the gen.Int64 generator will be used and the condition will
Expand All @@ -25,7 +25,7 @@ be tested for the full range of int64 numbers.
To adapt this one might register a generator for a specific type in an
arbitraries context. I.e. by adding
arbitraries.RegisterGen(gen.Int64Range(-1000, 1000))
arbitraries.RegisterGen(gen.Int64Range(-1000, 1000))
any generated int64 number will be between -1000 and 1000.
*/
Expand Down
4 changes: 4 additions & 0 deletions arbitrary/gen_for_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ func (a *Arbitraries) genForKind(rt reflect.Type) gopter.Gen {
},
}
})
case reflect.Array:
if elementGen := a.GenForType(rt.Elem()); elementGen != nil {
return gen.ArrayOfN(rt.Len(), elementGen)
}
case reflect.Slice:
if elementGen := a.GenForType(rt.Elem()); elementGen != nil {
return gen.SliceOf(elementGen)
Expand Down
30 changes: 30 additions & 0 deletions arbitrary/gen_for_kind_arrays_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package arbitrary_test

import (
"reflect"
"testing"

"github.com/leanovate/gopter/arbitrary"
)

func TestArbitrariesArrays(t *testing.T) {
arbitraries := arbitrary.DefaultArbitraries()

gen := arbitraries.GenForType(reflect.TypeOf([20]int{}))
value, ok := gen.Sample()
if !ok {
t.Errorf("Invalid value %#v", value)
}
if _, ok = value.([20]int); !ok {
t.Errorf("Invalid value %#v", value)
}

gen = arbitraries.GenForType(reflect.TypeOf([10]string{}))
value, ok = gen.Sample()
if !ok {
t.Errorf("Invalid value %#v", value)
}
if _, ok = value.([10]string); !ok {
t.Errorf("Invalid value %#v", value)
}
}
45 changes: 23 additions & 22 deletions commands/example_circularqueue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,32 +196,33 @@ var cbCommands = &commands.ProtoCommands{
// ... of course he did not implemented the bug, that was evil me
//
// The bug only occures on the following conditions:
// - the queue size has to be greater than 4
// - the queue has to be filled entirely once
// - Get operations have to be at least 5 elements behind put
// - The Put at the end of the queue and 5 elements later have to be non-zero
// - the queue size has to be greater than 4
// - the queue has to be filled entirely once
// - Get operations have to be at least 5 elements behind put
// - The Put at the end of the queue and 5 elements later have to be non-zero
//
// Lets see what gopter has to say:
//
// The output of this example will be
// ! circular buffer: Falsified after 96 passed tests.
// ARG_0: initialState=State(size=7, elements=[]) sequential=[Put(0) Put(0)
// Get Put(0) Get Put(0) Put(0) Get Put(0) Get Put(0) Get Put(-1) Put(0)
// Put(0) Put(0) Put(0) Get Get Put(2) Get]
// ARG_0_ORIGINAL (85 shrinks): initialState=State(size=7, elements=[])
// sequential=[Put(-1855365712) Put(-1591723498) Get Size Size
// Put(-1015561691) Get Put(397128011) Size Get Put(1943174048) Size
// Put(1309500770) Size Get Put(-879438231) Size Get Put(-1644094687) Get
// Put(-1818606323) Size Put(488620313) Size Put(-1219794505)
// Put(1166147059) Get Put(11390361) Get Size Put(-1407993944) Get Get Size
// Put(1393923085) Get Put(1222853245) Size Put(2070918543) Put(1741323168)
// Size Get Get Size Put(2019939681) Get Put(-170089451) Size Get Get Size
// Size Put(-49249034) Put(1229062846) Put(642598551) Get Put(1183453167)
// Size Get Get Get Put(1010460728) Put(6828709) Put(-185198587) Size Size
// Get Put(586459644) Get Size Put(-1802196502) Get Size Put(2097590857) Get
// Get Get Get Size Put(-474576011) Size Get Size Size Put(771190414) Size
// Put(-1509199920) Get Put(967212411) Size Get Put(578995532) Size Get Size
// Get]
//
// ! circular buffer: Falsified after 96 passed tests.
// ARG_0: initialState=State(size=7, elements=[]) sequential=[Put(0) Put(0)
// Get Put(0) Get Put(0) Put(0) Get Put(0) Get Put(0) Get Put(-1) Put(0)
// Put(0) Put(0) Put(0) Get Get Put(2) Get]
// ARG_0_ORIGINAL (85 shrinks): initialState=State(size=7, elements=[])
// sequential=[Put(-1855365712) Put(-1591723498) Get Size Size
// Put(-1015561691) Get Put(397128011) Size Get Put(1943174048) Size
// Put(1309500770) Size Get Put(-879438231) Size Get Put(-1644094687) Get
// Put(-1818606323) Size Put(488620313) Size Put(-1219794505)
// Put(1166147059) Get Put(11390361) Get Size Put(-1407993944) Get Get Size
// Put(1393923085) Get Put(1222853245) Size Put(2070918543) Put(1741323168)
// Size Get Get Size Put(2019939681) Get Put(-170089451) Size Get Get Size
// Size Put(-49249034) Put(1229062846) Put(642598551) Get Put(1183453167)
// Size Get Get Get Put(1010460728) Put(6828709) Put(-185198587) Size Size
// Get Put(586459644) Get Size Put(-1802196502) Get Size Put(2097590857) Get
// Get Get Get Size Put(-474576011) Size Get Size Size Put(771190414) Size
// Put(-1509199920) Get Put(967212411) Size Get Put(578995532) Size Get Size
// Get]
//
// Though this is not the minimal possible combination of command, its already
// pretty close.
Expand Down
18 changes: 11 additions & 7 deletions commands/example_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,19 @@ var buggyCounterCommands = &commands.ProtoCommands{
// implementation that only occurs if the counter is above 3.
//
// The output of this example will be
// ! buggy counter: Falsified after 45 passed tests.
// ARG_0: initial=0 sequential=[INC INC INC INC DEC GET]
// ARG_0_ORIGINAL (9 shrinks): initial=0 sequential=[DEC RESET GET GET GET
// RESET DEC DEC INC INC RESET RESET DEC INC RESET INC INC GET INC INC DEC
// DEC GET RESET INC INC DEC INC INC INC RESET RESET INC INC GET INC DEC GET
// DEC GET INC RESET INC INC RESET]
//
// ! buggy counter: Falsified after 45 passed tests.
// ARG_0: initial=0 sequential=[INC INC INC INC DEC GET]
// ARG_0_ORIGINAL (9 shrinks): initial=0 sequential=[DEC RESET GET GET GET
// RESET DEC DEC INC INC RESET RESET DEC INC RESET INC INC GET INC INC DEC
// DEC GET RESET INC INC DEC INC INC INC RESET RESET INC INC GET INC DEC GET
// DEC GET INC RESET INC INC RESET]
//
// I.e. gopter found an invalid state with a rather long sequence of arbitrary
// commands/function calls, and then shrank that sequence down to
// INC INC INC INC DEC GET
//
// INC INC INC INC DEC GET
//
// which is indeed the minimal set of commands one has to perform to find the
// bug.
func Example_buggyCounter() {
Expand Down
42 changes: 21 additions & 21 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@ Package gopter contain the main interfaces of the GOlang Property TestER.
A simple property test might look like this:
func TestSqrt(t *testing.T) {
properties := gopter.NewProperties(nil)
properties.Property("greater one of all greater one", prop.ForAll(
func(v float64) bool {
return math.Sqrt(v) >= 1
},
gen.Float64Range(1, math.MaxFloat64),
))
properties.Property("squared is equal to value", prop.ForAll(
func(v float64) bool {
r := math.Sqrt(v)
return math.Abs(r*r-v) < 1e-10*v
},
gen.Float64Range(0, math.MaxFloat64),
))
properties.TestingRun(t)
}
func TestSqrt(t *testing.T) {
properties := gopter.NewProperties(nil)
properties.Property("greater one of all greater one", prop.ForAll(
func(v float64) bool {
return math.Sqrt(v) >= 1
},
gen.Float64Range(1, math.MaxFloat64),
))
properties.Property("squared is equal to value", prop.ForAll(
func(v float64) bool {
r := math.Sqrt(v)
return math.Abs(r*r-v) < 1e-10*v
},
gen.Float64Range(0, math.MaxFloat64),
))
properties.TestingRun(t)
}
Generally a property is just a function that takes GenParameters and produces
a PropResult:
type Prop func(*GenParameters) *PropResult
type Prop func(*GenParameters) *PropResult
but usually you will use prop.ForAll, prop.ForAllNoShrink or arbitrary.ForAll.
There is also the commands package, which can be helpful for stateful testing.
Expand Down
13 changes: 7 additions & 6 deletions example_labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ func spookyCalculation(a, b int) int {
// Example_labels demonstrates how labels may help, in case of more complex
// conditions.
// The output will be:
// ! Check spooky: Falsified after 0 passed tests.
// > Labels of failing property: even result
// a: 3
// a_ORIGINAL (44 shrinks): 861384713
// b: 0
// b_ORIGINAL (1 shrinks): -642623569
//
// ! Check spooky: Falsified after 0 passed tests.
// > Labels of failing property: even result
// a: 3
// a_ORIGINAL (44 shrinks): 861384713
// b: 0
// b_ORIGINAL (1 shrinks): -642623569
func Example_labels() {
parameters := gopter.DefaultTestParameters()
parameters.Rng.Seed(1234) // Just for this example to generate reproducible results
Expand Down
4 changes: 3 additions & 1 deletion gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ func (g Gen) WithLabel(label string) Gen {
// SuchThat creates a derived generator by adding a sieve.
// f: has to be a function with one parameter (matching the generated value) returning a bool.
// All generated values are expected to satisfy
// f(value) == true.
//
// f(value) == true.
//
// Use this care, if the sieve to to fine the generator will have many misses which results
// in an undecided property.
func (g Gen) SuchThat(f interface{}) Gen {
Expand Down
62 changes: 62 additions & 0 deletions gen/array_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package gen

import (
"reflect"

"github.com/leanovate/gopter"
)

// ArrayOfN generates an array of generated elements with definied length
func ArrayOfN(desiredlen int, elementGen gopter.Gen, typeOverrides ...reflect.Type) gopter.Gen {
var typeOverride reflect.Type
if len(typeOverrides) > 1 {
panic("too many type overrides specified, at most 1 may be provided.")
} else if len(typeOverrides) == 1 {
typeOverride = typeOverrides[0]
}
return func(genParams *gopter.GenParameters) *gopter.GenResult {
result, elementSieve, elementShrinker := genArray(elementGen, genParams, desiredlen, typeOverride)

genResult := gopter.NewGenResult(result.Interface(), ArrayShrinkerOne(elementShrinker))
if elementSieve != nil {
genResult.Sieve = func(v interface{}) bool {
rv := reflect.ValueOf(v)
return rv.Len() == desiredlen && forAllSieve(elementSieve)(v)
}
} else {
genResult.Sieve = func(v interface{}) bool {
return reflect.ValueOf(v).Len() == desiredlen
}
}
return genResult
}
}

func genArray(elementGen gopter.Gen, genParams *gopter.GenParameters, desiredlen int, typeOverride reflect.Type) (reflect.Value, func(interface{}) bool, gopter.Shrinker) {
element := elementGen(genParams)
elementSieve := element.Sieve
elementShrinker := element.Shrinker

sliceType := typeOverride
if sliceType == nil {
sliceType = element.ResultType
}

arrayType := reflect.ArrayOf(desiredlen, sliceType)
result := reflect.New(arrayType).Elem()

for i := 0; i < desiredlen; i++ {
value, ok := element.Retrieve()

if ok {
if value == nil {
result.Index(i).Set(reflect.Zero(sliceType))
} else {
result.Index(i).Set(reflect.ValueOf(value))
}
}
element = elementGen(genParams)
}

return result, elementSieve, elementShrinker
}
36 changes: 36 additions & 0 deletions gen/array_of_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gen_test

import (
"testing"

"github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen"
)

func TestArrayOfN(t *testing.T) {
genParams := gopter.DefaultGenParameters()
genParams.MaxSize = 50
elementGen := gen.Const("element")
arrayGen := gen.ArrayOfN(20, elementGen)

for i := 0; i < 100; i++ {
sample, ok := arrayGen(genParams).Retrieve()

if !ok {
t.Error("Sample was not ok")
}
strings, ok := sample.([20]string)
if !ok {
t.Errorf("Sample not slice of string: %#v", sample)
} else {
if len(strings) > 50 {
t.Errorf("Sample has invalid length: %#v", len(strings))
}
for _, str := range strings {
if str != "element" {
t.Errorf("Sample contains invalid value: %#v", sample)
}
}
}
}
}
Loading

0 comments on commit 69954c9

Please sign in to comment.