-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Shrinking interface confusion - how to write a custom shrinker? #6
Comments
Okay so slightly hacky (and i'm not sure that using the (This is the func (g Gen) MapWithShrinker(f interface{}) Gen {
mapperVal := reflect.ValueOf(f)
mapperType := mapperVal.Type()
if mapperVal.Kind() != reflect.Func {
panic(fmt.Sprintf("Param of MapWithShrinker has to be a func, but is %v", mapperType.Kind()))
}
if mapperType.NumIn() != 1 {
panic(fmt.Sprintf("Param of MapWithShrinker has to be a func with one param, but is %v", mapperType.NumIn()))
} else {
genResultType := g(gopter.DefaultGenParameters()).ResultType
if !genResultType.AssignableTo(mapperType.In(0)) {
panic(fmt.Sprintf("Param of Map WithShrinkerhas to be a func with one param assignable to %v, but is %v", genResultType, mapperType.In(0)))
}
}
if mapperType.NumOut() != 1 {
panic(fmt.Sprintf("Param of MapWithShrinker has to be a func with one return value, but is %v", mapperType.NumOut()))
}
outType := mapperType.Out(0)
return func(genParams *GenParameters) *GenResult {
history := make(map[interface{}]interface{})
result := g(genParams)
shrinker := func(value interface{}) gopter.Shrink {
inputGens := history[value]
s := result.Shrinker(inputGens).Filter(result.Sieve)
return func() (interface{}, bool) {
retrived, valid := s()
if !valid {
return nil, false
}
new := mapperVal.Call([]reflect.Value{reflect.ValueOf(retrived)})[0].Interface()
history[new] = retrived
return new, true
}
}
value, ok := result.RetrieveAsValue()
if ok {
mapped := mapperVal.Call([]reflect.Value{value})[0]
history[mapped.Interface()] = value.Interface()
return &GenResult{
Shrinker: shrinker,
result: mapped.Interface(),
Labels: result.Labels,
ResultType: outType,
}
}
return &GenResult{
Shrinker: NoShrinker,
result: nil,
Labels: result.Labels,
ResultType: outType,
}
}
} |
Hi, I don't think that this will really work in all cases. Actually the main problem for mapping is that I distinguish between "Shrinker" and "Shrink". A Shrink is no more than an iterator of values and a Shrinker is a factory for Shrink's for a specific start-value. Mapping a Shrink can be done with the same mapping function as the mapping the generator (i.e. (string, string) -> struct in your case). Mapping the Shrinker though requires the reverse function struct -> (string, string) which might not be well-defined in all cases. The reason why gen.Result contains a Srhinker instead of just a Shrink for the generated value (which would make mapping easier) is that prop.ForAll has to be able to start over a Shrink from intermediate points. Simple example: Let's say you have a function that takes an integer parameter and fails for some values,. Internally the following happens:
I hope this helps clarifying the problem a bit. For your specific example. The best shrinker I could come up with looked like this:
Unluckily I could not find a suitable example to demonstrate its usefullness |
Thanks - that's a very useful write up. I'll come back and read it a few more times and see if I can't come up with an addition to the docs. I might have some more follow up questions too. |
FWIW, I'm also having trouble understanding the Shrink / Shrinker interface while trying to write a customer shrinker. Some docs and examples in this area would be really appreciated. The comments above help somewhat. It would be great to get that information incorporated into the documentation somehow. I don't think my problems are resolved by this information, though. I have a shrinker which isn't as sophisticated as the int shrinker described above (fwiw I've previously looked at (and modified!) the implementation of the int shrinker but I never understood why it worked the way it works until I read the comment above). My shrinker is similar to a slice shrinker but elements in the slice need to be adjusted to account for whatever element is removed. The shrinker makes a new slice and then replaces the original value with the modified value before returning it. This makes it easier for it to produce the next smallest value (as compared to re-shrinking the original by a greater amount on the next iteration). This seems to be a different approach than the built-in shrinkers take (they seem to keep the original forever and use extra smarts to jump straight to the next most-shrunk value during iteration). I could implement something more like them ... but I don't see why the approach I've taken fails (and the failure mode is really bizarre, my slice keeps getting bigger and bigger... as if Gopter is inserting new elements in between iterations?). |
FWIW, I tweaked my shrinker so it returns a copy of the internal state and this solved the problem. |
Yes, good point. Mutable structs are kind of a weakness in this case. First thing I did is to add a special warning in the docs. I thought this over a bit and might have come up with a solution (yet to be fully implemented): The general idea is, to derive a complex generator using a bijective mapper (i.e. conversion in both directions). That way it should be possible to map the generator as well as its shrinker. Roughly the interface would look like this: DeriveGen(
func(a int, b string) *MyStruct {
return &MyStruct{a:a, b:b}
},
func(m *MyStruct) (int, string) {
return m.a, m.b
},
gen.Integer(),
gen.AnyString(),
) (see the new branch for a bit more detail). |
I think it would be an improvement, yes! Is the reverse operation needed? It's not possible to remember the generated values and go from there? |
As the interface of Shrinker and Shrink is right now, I fear the reverse operation is needed. Anyhow the version in the branch should now work, I "just" have to complete the tests, then I'll merge it back to the master branch |
I'm just getting started with gopter (also being fairly new to property based testing in general) and I was trying to write a custom generator for a struct type of mine with private fields/initalizers.
A simplified example:
This works but as documented
Map
looses the shrinker fromCombineGens
. (Okay, slightly surprising but documented.)My attempt at writing a version that adds a Shrinker ran into some problems - it seemed to loop and not shrink as well (or almost at all) as the CombineGen case. I was trying this:
I would have expected it to create the shrinker once and call it again, but it seems to create a new version of the shrinker many times which means it resets the shrink (because I'm ignoring the input
value
to try and get back at the underlying generators.I guess this is a long way of saying: what is the exact interface of the Shrinkers? What is the Shrinker called with? When is it called? How often?
The text was updated successfully, but these errors were encountered: