Produce the next value for this Generator.
Produce the next value for this Generator.
This is the heart and soul of Generator -- it is the one function that you are required to implement when creating a new one. It takes several fields describing the state of the current evaluation, and returns the next value to try, along with the new state.
The state consists of three fields:
This function returns a Tuple of three fields:
edges
List, it should usually return the head as the next
value, and the tail as the remainder after that.Randomizer
functions return a new Randomizer
. If you
didn't use it, just return the one that was passed in.
the size to generate, if that is relevant for this Generator
the remaining edge cases to be tried
the Randomizer to use if you need "random" data
a Tuple of the next value, the remaining edges, and the resulting Randomizer, as described above.
Some simple, "ordinary" values of type T.
Some simple, "ordinary" values of type T.
canonicals are used for certain higher-order functions, mainly during shrink.
For example, when the system is trying to simplify a List[T]
, it will look for
canonical values of T to try putting into that simpler list, to see if that still
causes the property to fail.
For example, a few of the common types provide these canonicals:
Int
: 0, 1, -1, 2, -2, 3, -3Char
: the letters and digitsString
: single-charactor Strings of the letter and digitsYou do not have to provide canonicals for a Generator. By default, this simply returns an empty Iterator.
This function takes a Randomizer to use as a parameter, in case canonical generation for this type has a random element to it. If you use this Randomizer, return the next one. If you don't use it, just use the passed-in one.
a Randomizer to use if this function requires any random data
the canonical values for this type (if any), and the next Randomizer
Support for filtering in for comprehensions.
Support for filtering in for comprehensions.
This means the same thing is does for all standard Scala Monads: it applies a filter function
to this Generator. If you use an if
clause in a for
comprehension, this is the function
that will be called.
It is closely related to Generator.withFilter, but is the older form.
The default implementation of this has a safety check, such that if an enormous number of values (100000 by default) are rejected by the filter in a row, it aborts in order to prevent infinite loops. If this occurs, you should probably rewrite your generator to not use a filter.
You generally should not need to override this.
the actual filter function, which takes a value of type T and says whether to include it or not
a Generator that only produces values that pass this filter.
The usual Monad function, to allow Generators to be composed together.
The usual Monad function, to allow Generators to be composed together.
This is primarily here to support the ability to create new Generators easily using for comprehensions. For example, say that you needed a Generator that produces a Tuple of an Int and a Float. You can write that easily:
val tupleGen = for { a <- Generator.intGenerator b <- Generator.floatGenerator } yield (a, b)
That is, flatMap takes a function that returns a Generator, and combines it with this
Generator, to produce a new Generator. That function may make use of a value from
this Generator (that is part of the standard contract of flatMap
), but usually does not.
the type produced by the other Generator
a function that takes a value from this Generator, and returns another Generator
a Generator that is this one and the other one, composed together
Prepare a list of edge-case values ("edges") for testing this type.
Prepare a list of edge-case values ("edges") for testing this type.
The contents of this list are entirely up to the Generator. It is allowed to be empty, but it is a good idea to think about whether there are appropriate edge cases for this type. (By default, this is empty, so you can get your Generator working first, and think about edge cases after that.)
It is common, but not required, to randomize the order of the edge cases here. If so, you should use the Randomizer.shuffle function for this, so that the order is reproducible if something fails. If you don't use the Randomizer, just return it unchanged as part of the returned tuple.
Note the maxLength parameter. This is the number of tests to be run in total. So the list of returned edges should be no longer than this.
the maximum size of the returned List
the Randomizer that should be used if you want randomization of the edges
a Tuple: the list of edges, and the next Randomizer
Given a function from types T to U, return a new Generator that produces values of type U.
Given a function from types T to U, return a new Generator that produces values of type U.
For example, say that you needed a Generator that only creates even Ints. We already have Generator.intGenerator, so one way to write this would be:
val evenGen: Generator[Int] = Generator.intGenerator.map { i => val mod = i % 2 if ((mod == 1) || (mod == -1)) // It is odd, so the one before it is even: i - 1 else // Already even i }
This often makes it much easier to create a new Generator, if you have an existing one you can base it on.
the type of Generator you want to create
a function from T to U
a new Generator, based on this one and the given transformation function
Fetch a generated value of type T.
Fetch a generated value of type T.
sample allows you to experiment with this Generator in a convenient, ad-hoc way. Each time you call it, it will create a new Randomizer and a random size, and then calls next to generate a value.
You should not need to override this method; it is here to let you play with your Generator as you build it, and see what sort of values are actually coming out.
a generated value of type T
Generate a number of values of type T.
Generate a number of values of type T.
This is essentially the same as sample, and all the same comments apply, but this will generate as many values as you ask for.
the number of values to generate
a List of size length
, of randomly-generated values
Given a value of type T, produce some smaller/simpler values if that makes sense.
Given a value of type T, produce some smaller/simpler values if that makes sense.
When a property evaluation fails, the test system tries to simplify the failing case, to make debugging easier. How this simplification works depends on the type of Generator. For example, if it is a Generator of Lists, it might try with shorter Lists; if it is a Generator of Strings, it might try with shorter Strings.
The critical rule is that the values returned from shrink
must be smaller/simpler than
the passed-in value, and must not include the passed-in value. This is to ensure
that the simplification process will always complete, and not go into an infinite loop.
This function receives a Randomizer, in case there is a random element to the simplification process. If you use the Randomizer, you should return the next one; if not, simply return the passed-in one.
You do not have to implement this function. If you do not, it will return an empty Iterator, and the test system will not try to simplify failing values of this type.
This function returns a Tuple. The first element should be an Iterator that returns simplified values, and is empty when there are no more. The second element is the next Randomizer, as discussed above.
a value that failed property evaluation
a Randomizer to use, if you need random data for the shrinking process
a Tuple of the shrunk values and the next Randomizer
Support for filtering in for comprehensions.
Support for filtering in for comprehensions.
This means the same thing is does for all standard Scala Monads: it applies a filter function
to this Generator. If you use an if
clause in a for
comprehension, this is the function
that will be called.
The default implementation of this has a safety check, such that if an enormous number of values (100000 by default) are rejected by the filter in a row, it aborts in order to prevent infinite loops. If this occurs, you should probably rewrite your generator to not use a filter.
You generally should not need to override this.
the actual filter function, which takes a value of type T and says whether to include it or not
a Generator that only produces values that pass this filter.
Base type for all Generators.
A Generator produces a stream of values of a particular type. This is usually a mix of randomly-created values (generally built using a Randomizer), as well as some well-known edge cases that tend to cause bugs in real-world code.
For example, consider an intGenerator that produces a sequence of Ints. Some of these will be taken from Randomizer.nextInt, which may result in any possible Int, so the values will be a very random mix of numbers. But it will also produce the known edge cases of Int:
The list of appropriate edge cases will vary from type to type, but they should be chosen so as to exercise the type broadly, and at extremes.
Creating Your Own Generators
Generator.intGenerator, and Generators for many other basic types, are already built into the system, so you can just use them. You can (and should) define Generators for your own types, as well.
In most cases, you do not need to write Generators from scratch -- Generators for most non-primitive types can be composed using for comprehensions, as described in the section Composing Your Own Generators in the documentation for the org.scalatest.prop pacakge. You can also often create them using the CommonGenerators.instancesOf method. You should only need to write a Generator from scratch for relatively primitive types, that aren't composed of other types.
If you decide that you do need to build a Generator from scratch, here is a rough outline of how to go about it.
First, look at the source code for some of the Generators in the Generator companion object. These follow a pretty standard pattern, that you will likely want to follow.
Size
Your Generator may optionally have a concept of size. What this means varies from type to type: for a String it might be the number of characters, whereas for a List it might be the number of elements. The test system will try using the Generator with a variety of sizes; you can control the maximum and minimum sizes via Configuration.
Decide whether the concept of size is relevant for your type. If it is relevant, you should mix the HavingSize or HavingLength trait into your Generator, and you'll want to take it into account in your
next
andshrink
functions.Randomization
The Generator should do all of its "random" data generation using the Randomizer instance passed in to it, and should return the next Randomizer with its results. Randomizer produces intentionally pseudo-random data: it looks reasonably random, but is actually entirely deterministic based on the seed used to initialize it. By consistently using Randomizer, the Generator can be re-run, producing the same values, when given an identically-seeded Randomizer. This can often make debugging much easier, since it allows you to reproduce your "random" failures.
So figure out how to create a pseudo-random value of your type using Randomizer. This will likely involve writing a function similar to the various
nextT()
functions inside of Randomizer itself.next()
Using this randomization function, write a first draft of your Generator, filling in the
next()
method. This is the only required method, and should suffice to start playing with your Generator. Once this is working, you have a useful Generator.Edges
The edges are the edge cases for this type. You may have as many or as few edge cases as seem appropriate, but most types involve at least a few. Edges are generally values that are particularly big/full, or particularly small/empty. The test system will prioritize applying the edge cases to the property, since they are assumed to be the values most likely to cause failures.
Figure out some appropriate edge cases for your type. Override
initEdges()
to return those, and enhancenext()
to produce them ahead of the random values. Identifying these will tend to make your property checks more effective, by catching these edge cases early.Canonicals
Now figure out some canonical values for your type -- a few common, ordinary values that are frequently worth testing. These will be used when shrinking your type in higher-order Generators, so it is helpful to have some. Override the
canonicals()
method to return these.Canonicals should always be in order from "smallest" to less-small, in the shrinking sense. This is not the same thing as starting with the lowest number, though! For example, the canonicals for Generator.byteGenerator are:
Zero is "smallest" -- the most-shrunk Byte.
Shrinking
Optionally but preferably, your Generator can have a concept of shrinking. This starts with a value that is known to cause the property evaluation to fail, and produces a list of smaller/simpler values, to see if those also fail. So for example, if a String of length 15 causes a failure, its Generator could try Strings of length 3, and then 1, and then 0, to see if those also cause failure.
You to not have to implement the Generator.shrink method, but it is helpful to do so when it makes sense; the test system will use that to produce smaller, easier-to-debug examples when something fails.
One important rule: the values returned from
shrink
must always be smaller than -- not equal to -- the values passed in. Otherwise, an infinite loop can result. Also, similar to Canonicals, the "smallest" values should be returned at the front of this Iterator, with less-small values later.the type that this Generator produces