-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from anexia-it/codegen-object-tests
Code generator generating tests for Objects and optional hook interfaces
- Loading branch information
Showing
10 changed files
with
570 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,96 @@ | ||
# Testing | ||
# Dev workflows | ||
|
||
`Ginkgo` is used as test suite to implement integration tests for each of the sdk packages. The integration tests are | ||
located under `./tests`. | ||
## Testing | ||
|
||
To execute the complete test suite just run `ginkgo ./tests`. | ||
To specify a subset of testcases use the `-focus` flag and provide a regex to match the description of the tests | ||
used with `Describe(...)`. For example to execute all `core` tests run `ginkgo -focus="Core API endpoint tests"`. | ||
`Ginkgo` is used as test suite to implement integration tests for each of the sdk packages. The integration tests | ||
are located under `./tests`. | ||
|
||
To execute the complete test suite just run `ginkgo ./tests`.\ | ||
To specify a subset of testcases use the `-focus` flag and provide a regex to match the description of the tests | ||
used with `Describe(...)`. For example to execute all `core` tests run `ginkgo -focus="Core API endpoint tests"`. | ||
|
||
|
||
## Code generator | ||
|
||
Our build tools include a small code generator, currently only used to help with some tasks related to `Object`s, | ||
the interface needed to be compatible with the generic client. It's code lives in `/tools/object_generator.go`. | ||
|
||
This was built to deal with go's interfaces being implicit. The generic client allows `Object`s to implement | ||
additional interfaces to hook into parts of the request processing and we want to make sure `Object`s always | ||
implement the interfaces they think they do. The code generator is used to generate some tests (using `ginkgo` and | ||
`gomega`) to make sure the interfaces specified in the magic comment are really implemented. These tests will only | ||
be run when there is a spec runner test file for the package already - but you should have that anyway. | ||
|
||
Only files with names not starting with `.`, ending with `.go` and not ending with `_test.go` are parsed, which | ||
translates to every non-hidden non-test go file. | ||
|
||
|
||
### Workflow & CI | ||
|
||
When changing anything on a magic comment, a type marked with a magic comment or the code generator itself, you | ||
have to run `make generate` to re-generate files. The CI will check if `make generate` changes anything, failing | ||
if something was changed influencing the generated code without re-generating it. | ||
|
||
|
||
### Magic comment format | ||
|
||
Comments always start with `// anxcloud:`, that's what the generator is looking for. Single space required between the | ||
comment-starting `//` and `anxcloud:`. To process the comment, this common prefix is stripped from it. | ||
|
||
The payload of the comment (what is left after stripping the prefix) is then split by `:` to get some `specs`, | ||
which are then split by `=` to have a spec name and value. Not all specs have a value, if a spec has multiple | ||
values, it has them separated by `,`. | ||
|
||
Some examples: | ||
|
||
```go | ||
// anxcloud:object | ||
// -> this is parsed to having the spec called 'object' with no value | ||
|
||
// anxcloud:object:hooks=ResponseBodyHook | ||
// -> this is parsed to having the spec called 'object' with no value | ||
// and the spec 'hooks' with value 'ResponseBodyHook'. | ||
|
||
// anxcloud:object:hooks=ResponseBodyHook,ResponseDecodeHook | ||
// -> this is parsed to having the spec called 'object' with no value | ||
// and the spec 'hooks' with value 'ResponseBodyHook,ResponseDecodeHook'. | ||
// Code handling the 'hooks' spec will split the value by ',' to decode | ||
// single elements. | ||
``` | ||
|
||
Currently all the specs have to be given in the same comment line, placed above the `type` keyword they apply to | ||
**with one blank line in between**. The blank line ensures the magic comment isn't written into the documentation. | ||
|
||
Example how it looks in real world: | ||
|
||
```go | ||
// anxcloud:object:hooks=ResponseBodyHook | ||
|
||
// LoadBalancer describes a single LoadBalancer in Anexia LBaaS API. | ||
type LoadBalancer struct { | ||
// [...] | ||
} | ||
``` | ||
|
||
This example makes sure the type `LoadBalancer` does everything necessary to be usable with the generic client | ||
(`object` spec) and always implements the interface for the `ResponseBodyHook` correctly. | ||
|
||
|
||
#### Known specs and their usage | ||
|
||
* `object` | ||
|
||
| Usable on | Value | | ||
|-----------|--------| | ||
| types | (none) | | ||
|
||
Specifies the type is an `Object`, something usable with the generic API client. | ||
|
||
|
||
* `hooks` | ||
|
||
| Usable on | Value | | ||
|-----------|--------| | ||
| types | names of hook interfaces from `pkg/api/types` | | ||
|
||
Explicitly specifies the type implements the given interfaces. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package loadbalancer | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestLoadbalancer(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "test suite for Loadbalancer") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package loadbalancer | ||
|
||
import ( | ||
. "github.com/anexia-it/go-anxcloud/pkg/utils/test/gomega" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
|
||
"github.com/anexia-it/go-anxcloud/pkg/api/types" | ||
) | ||
|
||
var _ = Describe("Object Loadbalancer", func() { | ||
It("implements the interface types.Object", func() { | ||
var i types.Object | ||
o := Loadbalancer{} | ||
Expect(&o).To(ImplementInterface(&i)) | ||
}) | ||
It("implements the interface types.RequestBodyHook", func() { | ||
var i types.RequestBodyHook | ||
o := Loadbalancer{} | ||
Expect(&o).To(ImplementInterface(&i)) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package test | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/onsi/gomega/types" | ||
) | ||
|
||
type implementInterfaceMatcher struct { | ||
iface interface{} | ||
} | ||
|
||
// ImplementInterface succeeds if actual implements the given interface. | ||
// | ||
// The expected interface has to be passed as a pointer to variable of it, see the provided example. | ||
func ImplementInterface(i interface{}) types.GomegaMatcher { | ||
return implementInterfaceMatcher{ | ||
iface: i, | ||
} | ||
} | ||
|
||
func (ii implementInterfaceMatcher) Match(actual interface{}) (bool, error) { | ||
ifaceType := reflect.TypeOf(ii.iface) | ||
if ifaceType.Kind() != reflect.Ptr || ifaceType.Elem().Kind() != reflect.Interface { | ||
return false, errors.New("ImplementsInterface needs to have a pointer to a interface variable passed to it") | ||
} | ||
|
||
return reflect.TypeOf(actual).Implements(ifaceType.Elem()), nil | ||
} | ||
|
||
func (ii implementInterfaceMatcher) FailureMessage(actual interface{}) string { | ||
ifaceType := reflect.TypeOf(ii.iface).Elem() | ||
actualType := reflect.TypeOf(actual) | ||
|
||
name := actualType.Name() | ||
if actualType.Kind() == reflect.Ptr { | ||
name = "*" + actualType.Elem().Name() | ||
} | ||
|
||
return fmt.Sprintf("Type %v does not implement interface %v", name, ifaceType.Name()) | ||
} | ||
|
||
func (ii implementInterfaceMatcher) NegatedFailureMessage(actual interface{}) string { | ||
ifaceType := reflect.TypeOf(ii.iface).Elem() | ||
actualType := reflect.TypeOf(actual) | ||
|
||
name := actualType.Name() | ||
if actualType.Kind() == reflect.Ptr { | ||
name = "*" + actualType.Elem().Name() | ||
} | ||
|
||
return fmt.Sprintf("Type %v implements interface %v", name, ifaceType.Name()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/tools |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,36 @@ | ||
//go:build tools | ||
// +build tools | ||
|
||
package main | ||
|
||
import ( | ||
_ "github.com/client9/misspell/cmd/misspell" | ||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint" | ||
_ "github.com/katbyte/terrafmt" | ||
"fmt" | ||
"os" | ||
) | ||
|
||
var tools map[string]func() = make(map[string]func()) | ||
|
||
func usage() { | ||
fmt.Printf("Usage: %v command [flags]\n\nValid commands:\n", os.Args[0]) | ||
|
||
for tool := range tools { | ||
fmt.Printf(" %v\n", tool) | ||
} | ||
|
||
os.Exit(-1) | ||
} | ||
|
||
func main() { | ||
if len(os.Args) < 2 { | ||
usage() | ||
} | ||
|
||
tool := os.Args[1] | ||
|
||
if f, ok := tools[tool]; !ok { | ||
usage() | ||
} else { | ||
args := []string{os.Args[0] + " " + tool} | ||
args = append(args, os.Args[2:]...) | ||
os.Args = args | ||
|
||
f() | ||
} | ||
} |
Oops, something went wrong.