Skip to content

Commit

Permalink
Merge pull request #84 from anexia-it/codegen-object-tests
Browse files Browse the repository at this point in the history
Code generator generating tests for Objects and optional hook interfaces
  • Loading branch information
LittleFox94 committed Nov 26, 2021
2 parents 6233746 + bc0eb51 commit 6e6d43e
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 14 deletions.
11 changes: 10 additions & 1 deletion .github/workflows/code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ env:
jobs:
go-lint:
runs-on: ubuntu-latest
container: golang:1.14-stretch
container: golang:1.15-buster
steps:
- uses: actions/checkout@v2
- name: run linters
run: |
make tools
golangci-lint run --config .golangci.yml
make fmtcheck
codegen-uptodate:
runs-on: ubuntu-latest
container: golang:1.15-buster
steps:
- uses: actions/checkout@v2
- name: generate code
run: |
make generate
if [ -n "$(git status --porcelain=v1)" ]; then git add -N .; git diff; exit 1; fi
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ build: fmtcheck go-lint
go build -ldflags "$(GOLDFLAGS)" ./...

.PHONY: generate
generate:
generate: tools
# generate object tests
tools/tools object-generator --mode tests --in ./pkg/... --out xxgenerated_object_test.go
# run golang default generator
go generate ./...

.PHONY: depscheck
Expand Down Expand Up @@ -92,3 +95,4 @@ tools:
cd tools && go install github.com/client9/misspell/cmd/misspell
cd tools && go install github.com/golangci/golangci-lint/cmd/golangci-lint
cd tools && go install github.com/katbyte/terrafmt
cd tools && go build
100 changes: 94 additions & 6 deletions docs/Contribution.md
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.
2 changes: 2 additions & 0 deletions pkg/lbaas/loadbalancer/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type RuleInfo struct {
Name string `json:"name"`
}

// anxcloud:object:hooks=RequestBodyHook

// Loadbalancer holds the information of a load balancer instance.
type Loadbalancer struct {
CustomerIdentifier string `json:"customer_identifier"`
Expand Down
13 changes: 13 additions & 0 deletions pkg/lbaas/loadbalancer/loadbalancer_test.go
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")
}
22 changes: 22 additions & 0 deletions pkg/lbaas/loadbalancer/xxgenerated_object_test.go
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))
})
})
55 changes: 55 additions & 0 deletions pkg/utils/test/gomega/interface.go
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())
}
1 change: 1 addition & 0 deletions tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tools
38 changes: 32 additions & 6 deletions tools/main.go
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()
}
}

0 comments on commit 6e6d43e

Please sign in to comment.