Skip to content

Commit

Permalink
Auto-generate tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
q-uint committed May 29, 2023
1 parent 6823092 commit 0256ebc
Show file tree
Hide file tree
Showing 22 changed files with 2,459 additions and 627 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,4 @@ jobs:
- uses: aviate-labs/setup-dfx@v0.2.3
with:
dfx-version: 0.13.1
- run: |
make test
make test-ledger
- run: make test
7 changes: 1 addition & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test test-cover test-ledger gen gen-ic fmt
.PHONY: test test-cover gen gen-ic fmt

test:
go test -v -cover ./...
Expand All @@ -7,11 +7,6 @@ test-cover:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

test-ledger:
cd ic; dfx start --background --clean
cd ic/testdata; dfx deploy --no-wallet
cd ic; DFX=true go test -v icpledger_test.go; dfx stop

gen:
cd candid && go generate

Expand Down
5 changes: 4 additions & 1 deletion candid/idl/nat.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ func (n NatType) EncodeValue(v any) ([]byte, error) {
if !ok {
return nil, fmt.Errorf("invalid value: %v", v)
}
return leb128.EncodeUnsigned(v.BigInt())
if bi := v.BigInt(); bi != nil {
return leb128.EncodeUnsigned(bi)
}
return leb128.EncodeUnsigned(big.NewInt(0))
case 8:
v, err := encodeNat64(v)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions candid/idl/optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func (o OptionalType) EncodeValue(v any) ([]byte, error) {
return []byte{0x00}, nil
}
if v := reflect.ValueOf(v); v.Kind() == reflect.Ptr {
if v.IsNil() {
return []byte{0x00}, nil
}
return o.EncodeValue(v.Elem().Interface())
}
v_, err := o.Type.EncodeValue(v)
Expand Down
8 changes: 8 additions & 0 deletions candid/idl/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func StructToMap(value any) (map[string]any, error) {
m := make(map[string]any)
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
if !field.IsExported() {
continue
}

tag := ParseTags(field)
m[tag.Name] = v.Field(i).Interface()
}
Expand Down Expand Up @@ -135,6 +139,10 @@ func (record RecordType) String() string {
}

func (record RecordType) UnmarshalGo(raw any, _v any) error {
if raw == nil && record.Fields == nil {
return nil // Empty record.
}

m := make(map[string]any)
switch rv := reflect.ValueOf(raw); rv.Kind() {
case reflect.Map:
Expand Down
8 changes: 8 additions & 0 deletions common/cmotoko/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cmotoko

// Result is a generic type that represents either success (Ok) or failure (Err).
// It is used as the return type of functions which may fail.
type Result[T any, E any] struct {
Ok *T `ic:"ok,variant"`
Err *E `ic:"err,variant"`
}
8 changes: 8 additions & 0 deletions common/crust/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cmotoko

// Result is a generic type that represents either success (Ok) or failure (Err).
// It is used as the return type of functions which may fail.
type Result[T any, E any] struct {
Ok *T `ic:"Ok,variant"`
Err *E `ic:"Err,variant"`
}
103 changes: 87 additions & 16 deletions gen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ var (
templates map[string]*template.Template
)

func funcName(name string) string {
func funcName(prefix, name string) string {
if strings.HasPrefix(name, "\"") {
name = name[1 : len(name)-1]
}
var str string
for _, p := range strings.Split(name, "_") {
str += strings.ToUpper(string(p[0])) + p[1:]
}
if prefix != "" {
return fmt.Sprintf("%s.%s", prefix, str)
}
return str
}

Expand Down Expand Up @@ -66,6 +69,7 @@ func rawName(name string) string {
// Generator is a generator for a given service description.
type Generator struct {
AgentName string
ModulePath string
CanisterName string
PackageName string
ServiceDescription did.Description
Expand All @@ -92,8 +96,8 @@ func (g *Generator) Generate() ([]byte, error) {
switch definition := definition.(type) {
case did.Type:
definitions = append(definitions, agentArgsDefinition{
Name: funcName(definition.Id),
Type: g.dataToString(definition.Data),
Name: funcName("", definition.Id),
Type: g.dataToString("", definition.Data),
})
}
}
Expand All @@ -114,13 +118,13 @@ func (g *Generator) Generate() ([]byte, error) {
}
argumentTypes = append(argumentTypes, agentArgsMethodArgument{
Name: n,
Type: g.dataToString(t.Data),
Type: g.dataToString("", t.Data),
})
}

var returnTypes []string
for _, t := range f.ResTypes {
returnTypes = append(returnTypes, g.dataToString(t.Data))
returnTypes = append(returnTypes, g.dataToString("", t.Data))
}

typ := "Call"
Expand All @@ -130,7 +134,7 @@ func (g *Generator) Generate() ([]byte, error) {

methods = append(methods, agentArgsMethod{
RawName: name,
Name: funcName(name),
Name: funcName("", name),
Type: typ,
ArgumentTypes: argumentTypes,
ReturnTypes: returnTypes,
Expand All @@ -155,16 +159,74 @@ func (g *Generator) Generate() ([]byte, error) {
return io.ReadAll(&tmpl)
}

func (g *Generator) dataToString(data did.Data) string {
func (g *Generator) GenerateMock() ([]byte, error) {
var methods []agentArgsMethod
for _, service := range g.ServiceDescription.Services {
for _, method := range service.Methods {
name := rawName(method.Name)
f := method.Func

var argumentTypes []agentArgsMethodArgument
for i, t := range f.ArgTypes {
var n string
if (t.Name != nil) && (*t.Name != "") {
n = *t.Name
} else {
n = fmt.Sprintf("arg%d", i)
}
argumentTypes = append(argumentTypes, agentArgsMethodArgument{
Name: n,
Type: g.dataToString(g.PackageName, t.Data),
})
}

var returnTypes []string
for _, t := range f.ResTypes {
returnTypes = append(returnTypes, g.dataToString(g.PackageName, t.Data))
}

typ := "Call"
if f.Annotation != nil && *f.Annotation == did.AnnQuery {
typ = "Query"
}

methods = append(methods, agentArgsMethod{
RawName: name,
Name: funcName("", name),
Type: typ,
ArgumentTypes: argumentTypes,
ReturnTypes: returnTypes,
})
}
}
t, ok := templates["agent_test"]
if !ok {
return nil, fmt.Errorf("template not found")
}
var tmpl bytes.Buffer
if err := t.Execute(&tmpl, agentMockArgs{
AgentName: g.AgentName,
CanisterName: g.CanisterName,
PackageName: g.PackageName,
ModulePath: g.ModulePath,
UsedIDL: g.usedIDL,
Methods: methods,
}); err != nil {
return nil, err
}
return io.ReadAll(&tmpl)
}

func (g *Generator) dataToString(prefix string, data did.Data) string {
switch t := data.(type) {
case did.Blob:
return "[]byte"
case did.DataId:
return funcName(string(t))
return funcName(prefix, string(t))
case did.Func:
return "struct { /* NOT SUPPORTED */ }"
case did.Optional:
return fmt.Sprintf("*%s", g.dataToString(t.Data))
return fmt.Sprintf("*%s", g.dataToString(prefix, t.Data))
case did.Primitive:
switch t {
case "nat8", "nat16", "nat32", "nat64":
Expand Down Expand Up @@ -199,16 +261,16 @@ func (g *Generator) dataToString(data did.Data) string {
name := originalName
if n := field.Name; n != nil {
originalName = *n
name = funcName(*n)
name = funcName("", *n)
}
if l := len(name); l > sizeName {
sizeName = l
}
var typ string
if field.Data != nil {
typ = g.dataToString(*field.Data)
typ = g.dataToString(prefix, *field.Data)
} else {
typ = funcName(*field.NameData)
typ = funcName(prefix, *field.NameData)
}
for _, typ := range strings.Split(typ, "\n") {
if l := len(typ); l > sizeType {
Expand Down Expand Up @@ -257,15 +319,15 @@ func (g *Generator) dataToString(data did.Data) string {
typ string
}{originalName: name, name: name, typ: "struct{}"})
} else {
name := funcName(*field.Name)
name := funcName("", *field.Name)
if l := len(name); l > sizeName {
sizeName = l
}
var typ string
if field.Data != nil {
typ = g.dataToString(*field.Data)
typ = g.dataToString(prefix, *field.Data)
} else {
typ = funcName(*field.NameData)
typ = funcName(prefix, *field.NameData)
}
for _, typ := range strings.Split(typ, "\n") {
if l := len(typ); l > sizeType {
Expand All @@ -289,7 +351,7 @@ func (g *Generator) dataToString(data did.Data) string {
}
return fmt.Sprintf("struct {\n%s}", record)
case did.Vector:
return fmt.Sprintf("[]%s", g.dataToString(t.Data))
return fmt.Sprintf("[]%s", g.dataToString(prefix, t.Data))
default:
panic(fmt.Sprintf("unknown type: %T", t))
}
Expand Down Expand Up @@ -321,3 +383,12 @@ type agentArgsMethodArgument struct {
Name string
Type string
}

type agentMockArgs struct {
AgentName string
CanisterName string
PackageName string
ModulePath string
UsedIDL bool
Methods []agentArgsMethod
}
57 changes: 57 additions & 0 deletions gen/templates/agent_test.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Automatically generated by https://github.com/aviate-labs/agent-go.
package {{ .PackageName }}_test

import (
"github.com/aviate-labs/agent-go"
{{ if .UsedIDL }}"github.com/aviate-labs/agent-go/candid/idl"{{ end }}
"github.com/aviate-labs/agent-go/mock"
"github.com/aviate-labs/agent-go/principal"
"net/http/httptest"
"net/url"
"testing"

"{{ .ModulePath }}/{{ .PackageName }}"
)

// newAgent creates a new agent with the given (mock) methods.
// Runs a mock replica in the background.
func newAgent(methods []mock.Method) (*{{ .PackageName }}.Agent, error) {
replica := mock.NewReplica()
canisterId := principal.Principal{Raw: []byte("{{ .PackageName }}")}
replica.AddCanister(canisterId, methods)
s := httptest.NewServer(replica)
u, _ := url.Parse(s.URL)
a, err := {{ .PackageName }}.NewAgent(canisterId, agent.Config{
ClientConfig: &agent.ClientConfig{Host: u},
FetchRootKey: true,
})
if err != nil {
return nil, err
}
return a, nil
}
{{- range .Methods }}

// Test_{{ .Name }} tests the "{{ .RawName }}" method on the "{{ $.CanisterName }}" canister.
func Test_{{ .Name }}(t *testing.T) {
a, err := newAgent([]mock.Method{
{
Name: "{{ .RawName }}",
Arguments: []any{{ "{" }}{{ range $i, $e := .ArgumentTypes }}{{ if $i }}, {{ end }}new({{ $e.Type }}){{ end }}{{ "}" }},
Handler: func (request mock.Request) ([]any, error) {
return []any{{ "{" }}{{ range $i, $e := .ReturnTypes }}{{ if $i }}, {{ end }}*new({{ $e }}){{ end }}{{ "}" }}, nil
},
},
})
if err != nil {
t.Fatal(err)
}
{{ range $i, $e := .ArgumentTypes }}
var a{{ $i }} {{ $e.Type }}
{{- end }}
if {{ range .ReturnTypes }}_, {{ end }}err := a.{{ .Name }}({{ range $i, $e := .ArgumentTypes }}{{ if $i }}, {{ end }}a{{ $i }}{{ end }}); err != nil {
t.Fatal(err)
}

}
{{- end }}
Loading

0 comments on commit 0256ebc

Please sign in to comment.