Skip to content

Commit

Permalink
Generate deterministic examples (#3521)
Browse files Browse the repository at this point in the history
* Generate deterministic examples

The random generated is seeded with the API name.

* Leverage built-in sync map for readability

* Fix race condition in test
  • Loading branch information
raphael committed May 18, 2024
1 parent 10e59a1 commit 6ef86c8
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 55 deletions.
3 changes: 1 addition & 2 deletions codegen/generator/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import (
)

// Transport iterates through the roots and returns the files needed to render
// the transport code. It returns an error if the roots slice does not include
// at least one transport design.
// the transport code.
func Transport(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
var files []*codegen.File
for _, root := range roots {
Expand Down
79 changes: 64 additions & 15 deletions expr/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"fmt"
"math"
"regexp"
"regexp/syntax"
"strings"
"time"

regen "github.com/AnatolyRugalev/goregen"
googleuuid "github.com/google/uuid"
)

const (
Expand Down Expand Up @@ -209,23 +208,17 @@ func byFormat(a *AttributeExpr, r *ExampleGenerator) any {
FormatIP: r.IPv4Address().String(),
FormatURI: r.URL(),
FormatMAC: func() string {
res, err := regen.Generate(`([0-9A-F]{2}-){5}[0-9A-F]{2}`)
res, err := syntax.Parse(`([0-9A-F]{2}-){5}[0-9A-F]{2}`, 0)
if err != nil {
return "12-34-56-78-9A-BC"
}
return res
return patgen(res, r)
}(),
FormatCIDR: "192.168.100.14/24",
FormatRegexp: r.Characters(3) + ".*",
FormatRFC1123: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC1123), // to obtain a "fixed" rand
FormatUUID: func() string {
uuid, err := googleuuid.NewUUID()
if err != nil {
return "12345678-1234-1234-9232-123456789ABC"
}
return uuid.String()
}(),
FormatJSON: `{"name":"example","email":"[email protected]"}`,
FormatUUID: r.UUID(),
FormatJSON: `{"name":"example","email":"[email protected]"}`,
}[format]; ok {
return res
}
Expand All @@ -240,11 +233,67 @@ func byPattern(a *AttributeExpr, r *ExampleGenerator) any {
return false
}
pattern := a.Validation.Pattern
gen, err := regen.NewGenerator(pattern, &regen.GeneratorArgs{MaxUnboundedRepeatCount: 6})
re, err := syntax.Parse(pattern, syntax.Perl)
if err != nil {
return r.Name()
}
return gen.Generate()
return patgen(re.Simplify(), r)
}

func patgen(re *syntax.Regexp, r *ExampleGenerator) string {
switch re.Op {
case syntax.OpAlternate:
i := r.Int() % len(re.Sub)
return patgen(re.Sub[i], r)
case syntax.OpCapture:
return patgen(re.Sub[0], r)
case syntax.OpConcat:
var res strings.Builder
for _, sub := range re.Sub {
res.WriteString(patgen(sub, r))
}
return res.String()
case syntax.OpLiteral:
return string(re.Rune)
case syntax.OpStar:
var res strings.Builder
count := r.Int() % 3
for i := 0; i < count; i++ {
res.WriteString(patgen(re.Sub[0], r))
}
return res.String()
case syntax.OpPlus:
var res strings.Builder
count := r.Int()%2 + 1
for i := 0; i < count; i++ {
res.WriteString(patgen(re.Sub[0], r))
}
return res.String()
case syntax.OpQuest:
if r.Int()%2 == 0 {
return patgen(re.Sub[0], r)
}
return ""
case syntax.OpRepeat:
var res strings.Builder
for i := 0; i < re.Min; i++ {
res.WriteString(patgen(re.Sub[0], r))
}
return res.String()
case syntax.OpCharClass:
var chars []rune
for i := 0; i < len(re.Rune); i += 2 {
start, end := re.Rune[i], re.Rune[i+1]
for j := start; j <= end; j++ {
chars = append(chars, j)
}
}
return string(chars[r.Int()%len(chars)])
case syntax.OpAnyChar, syntax.OpAnyCharNotNL:
return r.Characters(1)
default:
return ""
}
}

func byMinMax(a *AttributeExpr, r *ExampleGenerator) any {
Expand Down
2 changes: 1 addition & 1 deletion expr/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestByPattern(t *testing.T) {
ExpectedMaxLen int
}{
{"not-a-regexp", "foo", 3},
{"max-len", "foo.*", 9},
{"max-len", "foo[a-z]+", 9},
{"max-len-2", "^/api/example/[0-9]+$", 19},
}
r := expr.NewRandom("test")
Expand Down
11 changes: 11 additions & 0 deletions expr/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expr
import (
"crypto/md5"
"encoding/binary"
"fmt"
"math/rand"
"net"
"strings"
Expand Down Expand Up @@ -53,6 +54,8 @@ type Randomizer interface {
URL() string
// Characters generates a n-character string example
Characters(n int) string
// UUID generates a random v4 UUID
UUID() string
}

// NewRandom returns a random value generator seeded from the given string
Expand Down Expand Up @@ -165,6 +168,13 @@ func (r *FakerRandomizer) URL() string {
func (r *FakerRandomizer) Characters(n int) string {
return r.faker.Characters(n)
}
func (r *FakerRandomizer) UUID() string {
uuid := make([]byte, 16)
r.rand.Read(uuid)
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}
func (r *FakerRandomizer) Name() string {
return r.faker.Name()
}
Expand Down Expand Up @@ -197,3 +207,4 @@ func (DeterministicRandomizer) IPv4Address() net.IP { return net.IPv4zero }
func (DeterministicRandomizer) IPv6Address() net.IP { return net.IPv6zero }
func (DeterministicRandomizer) URL() string { return "https://example.com/foo" }
func (DeterministicRandomizer) Characters(n int) string { return strings.Repeat("a", n) }
func (DeterministicRandomizer) UUID() string { return "550e8400-e29b-41d4-a716-446655440000" }
12 changes: 10 additions & 2 deletions expr/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expr
import (
"fmt"
"reflect"
"sort"

"goa.design/goa/v3/eval"
)
Expand Down Expand Up @@ -591,8 +592,15 @@ func (m *Map) Example(r *ExampleGenerator) any {
// which cannot be handled by json.Marshal.
func (m *Map) MakeMap(raw map[any]any) any {
ma := reflect.MakeMap(toReflectType(m))
for key, value := range raw {
ma.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value))
keys := make([]any, 0, len(raw))
for key := range raw {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return reflect.ValueOf(keys[i]).String() < reflect.ValueOf(keys[j]).String()
})
for _, key := range keys {
ma.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(raw[key]))
}
return ma.Interface()
}
Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module goa.design/goa/v3
go 1.20

require (
github.com/AnatolyRugalev/goregen v0.1.0
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
github.com/getkin/kin-openapi v0.124.0
github.com/go-chi/chi/v5 v5.0.12
Expand All @@ -15,7 +14,7 @@ require (
github.com/stretchr/testify v1.9.0
golang.org/x/text v0.15.0
golang.org/x/tools v0.21.0
google.golang.org/grpc v1.63.2
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1
)
Expand All @@ -35,5 +34,5 @@ require (
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
)
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE=
github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -62,10 +60,10 @@ golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
33 changes: 8 additions & 25 deletions grpc/middleware/canceler.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,44 +39,27 @@ import (
// ...
func StreamCanceler(ctx context.Context) grpc.StreamServerInterceptor {
var (
cancels = map[*context.CancelFunc]struct{}{}
cancelMu = new(sync.Mutex)
cancels sync.Map
canceling uint32
)

go func() {
<-ctx.Done()
atomic.StoreUint32(&canceling, 1)
cancelMu.Lock()
defer cancelMu.Unlock()
for cancel := range cancels {
cancels.Range(func(key any, value any) bool {
cancel := key.(*context.CancelFunc)
(*cancel)()
}
return true
})
}()
return grpc.StreamServerInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if atomic.LoadUint32(&canceling) == 1 {
return status.Error(codes.Unavailable, "server is stopping")
}
var (
cctx = ss.Context()
cancel context.CancelFunc
)
cctx, cancel = context.WithCancel(cctx)

// add the cancel function
cancelMu.Lock()
cancels[&cancel] = struct{}{}
cancelMu.Unlock()

// invoke rpc
cctx, cancel := context.WithCancel(ss.Context())
cancels.Store(&cancel, struct{}{})
err := handler(srv, NewWrappedServerStream(cctx, ss))

// remove the cancel function
cancelMu.Lock()
delete(cancels, &cancel)
cancelMu.Unlock()

// cleanup the WithCancel
cancels.Delete(&cancel)
cancel()

return err
Expand Down
4 changes: 3 additions & 1 deletion grpc/middleware/canceler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ func TestStreamCanceler(t *testing.T) {
}()

if err := grpcm.StreamCanceler(ctx)(nil, c.stream, stream, c.handler); err != nil {
t.Errorf("StreamCanceler error: %v", err)
if err.Error() != "server is stopping" {
t.Errorf("StreamCanceler error: %v", err)
}
}
})
}
Expand Down

0 comments on commit 6ef86c8

Please sign in to comment.