Go Interface Fuzzer is a fuzzy testing tool for Go interfaces used @Pusher. The goal of the project is to make it easier for developers to have confidence in the correctness of their programs by combining randomised testing with reference semantics.
Given an interface, a reference implementation, and some hints on how to generate function parameters and compare function return values, Go Interface Fuzzer will generate testing functions which can be used to check that the behaviour of an arbitrary other type implementing the interface behaves the same.
See the _examples
directory for a complete self-contained example.
Table of Contents
Project Status
The tool is stable and the interface fixed. New functionality, directives, and command-line arguments may be added, but existing usages will not suddenly stop working.
Getting Started
Installing
To start using Go Interface Fuzzer, install Go and run go get
:
$ go get github.com/pusher/go-interface-fuzzer
This will install the go-interface-fuzzer
command-line tool into
your $GOBIN
path, which defaults to $GOPATH/bin
.
Usage
The generated code in the _examples
directory is produced by
go-interface-fuzzer -c -o -f _examples/store.generated.go _examples/store.go
- The
-c
flag generates a complete source file, complete with package name and imports. - The
-o
flag writes the output to the filename given by the-f
flag. - The
-f
flag specifies the filename to use when writing output and resolving imports.
The generated code can be customised further, see the full help text
(go-interface-fuzzer --help
) for a complete flag listing.
The tool generates three functions, named after the interface used. With the example file, the following functions are be produced:
-
FuzzStoreWith(reference Store, test Store, rand *rand.Rand, maxops uint) error
Create a new reference store and test store, apply a randomly-generated list of actions, and bail out on inconsistency.
-
FuzzStore(makeTest (func(int) Store), rand *rand.Rand, maxops uint) error
Call
FuzzStoreWithReference
with the ModelStore as the reference one. -
FuzzTestStore(makeTest (func(int) Store), t *testing.T)
A test case parameterised by the store generating function, with a default maxops of 100.
By default Go Interface Fuzzer generates an incomplete fragment: no package name, no imports, just the three testing functions per interface.
Incorporating into the build
Go makes adding a code generation stage to your build process quite
simple, with the go generate
tool. To incorporate into your build,
add a comment to your source file:
//go:generate go-interface-fuzzer -c -o -f output_file.go input_file.go
Typically this would be added to the same file which defines the interface and provides the processing directives (see the next section), but that isn't required.
To then actually generate the file, run go generate
. It is not done
for you as part of go build
.
For further information on code generation in Go see "Generating code", on the Go blog.
Directives
An interface must be marked-up with some processing directives to direct the tool. These directives are given inside a single multi-line comment.
The minimum is just indicating that a fuzz tester should be generated for the interface, and how to produce a new value of the reference implementation type. For example:
/*
@fuzz interface: Store
@known correct: makeReferenceStore int
*/
type Store interface {
// ...
}
The fuzzer definition does not need to be immediately next to the interface, it can be anywhere in the source file.
See the _examples
directory for a complete self-contained example
using most of the directives.
@fuzz interface
(required)
This directive begins the definition of a fuzzer, and gives the name of the interface to test.
Example: @fuzz interface: Store
Argument syntax: InterfaceName
@known correct
(required)
This directive gives a function to produce a new value of the reference implementation. It specifies the parameters of the function, and whether the return type is a value or a pointer type.
The generated fuzzing function will expect a function argument with the same parameters to create a new value of the type under test.
Example: @known correct: makeReferenceStore int
Argument syntax: [&] FunctionName [ArgType1 ... ArgTypeN]
The presence of a &
means that this returns a value rather than a
pointer, and so a reference must be made to it.
@invariant
This directive specifies a property that must always hold. It is only checked for the test implementation, not the reference implementation.
Example: @invariant: %var.NumEntries() == len(%var.AsSlice())
Argument syntax: Expression
The argument is a Go expression that evaluates to a boolean, with
%var
replaced with the variable name.
@comparison
This directive specifies a function to use to compare two values. If not specified the reflection package is used.
Example: @comparison: *MessageIterator:CompareWith
Argument syntax: (Type:FunctionName | FunctionName Type)
In the method form, the target of the comparison is passed as the sole parameter; in the function form both are passed as parameters.
@generator
This directive specifies a function to generate a value of the
required type. It is passed a PRNG of type *rand.Rand
. If no
generator for a type is specified, the tool will attempt to produce a
default; and report an error otherwise.
Example: @generator: GenerateChannel model.Channel
Argument syntax: [!] FunctionName Type
The presence of a !
means that this is a stateful function: it is
also passed a state parameter and is expected to return a new state as
its second result.
@generator state
This directive supplies an initial state for stateful generators. It must be given if any generators are stateful. The initial state is any legal Go expression; it is just copied verbatim into the generated code.
Example: @generator state: InitialGeneratorState
Argument syntax: Expression
Defaults
The following default comparison operations are used if not overridden:
Type | Comparison |
---|---|
error |
Equal if both values are nil or non-nil . |
Everything else | reflect.DeepEqual |
The following default generator functions are used if not overridden:
Type | Generator |
---|---|
bool |
rand.Intn(2) == 0 |
byte |
byte(rand.Uint32()) |
complex64 |
complex(float32(rand.NormFloat64()), float32(rand.NormFloat64())) |
complex128 |
complex(rand.NormFloat64(), rand.NormFloat64()) |
float32 |
float32(rand.NormFloat64()) |
float64 |
rand.NormFloat64() |
int |
rand.Int() |
int8 |
int8(rand.Int()) |
int16 |
int16(rand.Int()) |
int32 |
rand.Int31() |
int64 |
rand.Int63() |
rune |
rune(rand.Int31()) |
uint |
uint(rand.Uint32()) |
uint8 |
uint8(rand.Uint32()) |
uint16 |
uint16(rand.Uint32()) |
uint32 |
rand.Uint32() |
uint64 |
`(uint64(rand.Uint32()) << 32) |
Everything else | No default |
Other Uses
Regression testing
Although the motivating use-case for Go Interface Fuzzer was an interface with two implementations that should have identical behaviour, there is nothing preventing the use of multiple versions of the same implementation. This facilitates regression testing, although at the cost of needing to keep the old implementation around.
Here are the concrete steps you would need to follow to do this:
- Make a copy of your current implementation, with a new name.
- Write a new implementation.
- Use the current implementation as the reference implementation for the generated fuzzer.
This isn't quite the same as reference correctness, as any bugs in the old implementation which the new fixes will be reported as an error in the new implementation.
Assertion-only testing
By supplying the same implementation as both the reference and the test implementation, the fuzz tester will simply check invariants. Although, it'll be a little slow, because every operation is being performed twice.
In the future, there will probably be an "invariant-only" mode of operation.