Skip to content

Commit 6ef86c8

Browse files
authored
Generate deterministic examples (#3521)
* Generate deterministic examples The random generated is seeded with the API name. * Leverage built-in sync map for readability * Fix race condition in test
1 parent 10e59a1 commit 6ef86c8

File tree

9 files changed

+104
-55
lines changed

9 files changed

+104
-55
lines changed

codegen/generator/transport.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import (
1010
)
1111

1212
// Transport iterates through the roots and returns the files needed to render
13-
// the transport code. It returns an error if the roots slice does not include
14-
// at least one transport design.
13+
// the transport code.
1514
func Transport(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
1615
var files []*codegen.File
1716
for _, root := range roots {

expr/example.go

+64-15
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import (
44
"fmt"
55
"math"
66
"regexp"
7+
"regexp/syntax"
8+
"strings"
79
"time"
8-
9-
regen "github.com/AnatolyRugalev/goregen"
10-
googleuuid "github.com/google/uuid"
1110
)
1211

1312
const (
@@ -209,23 +208,17 @@ func byFormat(a *AttributeExpr, r *ExampleGenerator) any {
209208
FormatIP: r.IPv4Address().String(),
210209
FormatURI: r.URL(),
211210
FormatMAC: func() string {
212-
res, err := regen.Generate(`([0-9A-F]{2}-){5}[0-9A-F]{2}`)
211+
res, err := syntax.Parse(`([0-9A-F]{2}-){5}[0-9A-F]{2}`, 0)
213212
if err != nil {
214213
return "12-34-56-78-9A-BC"
215214
}
216-
return res
215+
return patgen(res, r)
217216
}(),
218217
FormatCIDR: "192.168.100.14/24",
219218
FormatRegexp: r.Characters(3) + ".*",
220219
FormatRFC1123: time.Unix(int64(r.Int())%1454957045, 0).UTC().Format(time.RFC1123), // to obtain a "fixed" rand
221-
FormatUUID: func() string {
222-
uuid, err := googleuuid.NewUUID()
223-
if err != nil {
224-
return "12345678-1234-1234-9232-123456789ABC"
225-
}
226-
return uuid.String()
227-
}(),
228-
FormatJSON: `{"name":"example","email":"[email protected]"}`,
220+
FormatUUID: r.UUID(),
221+
FormatJSON: `{"name":"example","email":"[email protected]"}`,
229222
}[format]; ok {
230223
return res
231224
}
@@ -240,11 +233,67 @@ func byPattern(a *AttributeExpr, r *ExampleGenerator) any {
240233
return false
241234
}
242235
pattern := a.Validation.Pattern
243-
gen, err := regen.NewGenerator(pattern, &regen.GeneratorArgs{MaxUnboundedRepeatCount: 6})
236+
re, err := syntax.Parse(pattern, syntax.Perl)
244237
if err != nil {
245238
return r.Name()
246239
}
247-
return gen.Generate()
240+
return patgen(re.Simplify(), r)
241+
}
242+
243+
func patgen(re *syntax.Regexp, r *ExampleGenerator) string {
244+
switch re.Op {
245+
case syntax.OpAlternate:
246+
i := r.Int() % len(re.Sub)
247+
return patgen(re.Sub[i], r)
248+
case syntax.OpCapture:
249+
return patgen(re.Sub[0], r)
250+
case syntax.OpConcat:
251+
var res strings.Builder
252+
for _, sub := range re.Sub {
253+
res.WriteString(patgen(sub, r))
254+
}
255+
return res.String()
256+
case syntax.OpLiteral:
257+
return string(re.Rune)
258+
case syntax.OpStar:
259+
var res strings.Builder
260+
count := r.Int() % 3
261+
for i := 0; i < count; i++ {
262+
res.WriteString(patgen(re.Sub[0], r))
263+
}
264+
return res.String()
265+
case syntax.OpPlus:
266+
var res strings.Builder
267+
count := r.Int()%2 + 1
268+
for i := 0; i < count; i++ {
269+
res.WriteString(patgen(re.Sub[0], r))
270+
}
271+
return res.String()
272+
case syntax.OpQuest:
273+
if r.Int()%2 == 0 {
274+
return patgen(re.Sub[0], r)
275+
}
276+
return ""
277+
case syntax.OpRepeat:
278+
var res strings.Builder
279+
for i := 0; i < re.Min; i++ {
280+
res.WriteString(patgen(re.Sub[0], r))
281+
}
282+
return res.String()
283+
case syntax.OpCharClass:
284+
var chars []rune
285+
for i := 0; i < len(re.Rune); i += 2 {
286+
start, end := re.Rune[i], re.Rune[i+1]
287+
for j := start; j <= end; j++ {
288+
chars = append(chars, j)
289+
}
290+
}
291+
return string(chars[r.Int()%len(chars)])
292+
case syntax.OpAnyChar, syntax.OpAnyCharNotNL:
293+
return r.Characters(1)
294+
default:
295+
return ""
296+
}
248297
}
249298

250299
func byMinMax(a *AttributeExpr, r *ExampleGenerator) any {

expr/example_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestByPattern(t *testing.T) {
1818
ExpectedMaxLen int
1919
}{
2020
{"not-a-regexp", "foo", 3},
21-
{"max-len", "foo.*", 9},
21+
{"max-len", "foo[a-z]+", 9},
2222
{"max-len-2", "^/api/example/[0-9]+$", 19},
2323
}
2424
r := expr.NewRandom("test")

expr/random.go

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package expr
33
import (
44
"crypto/md5"
55
"encoding/binary"
6+
"fmt"
67
"math/rand"
78
"net"
89
"strings"
@@ -53,6 +54,8 @@ type Randomizer interface {
5354
URL() string
5455
// Characters generates a n-character string example
5556
Characters(n int) string
57+
// UUID generates a random v4 UUID
58+
UUID() string
5659
}
5760

5861
// NewRandom returns a random value generator seeded from the given string
@@ -165,6 +168,13 @@ func (r *FakerRandomizer) URL() string {
165168
func (r *FakerRandomizer) Characters(n int) string {
166169
return r.faker.Characters(n)
167170
}
171+
func (r *FakerRandomizer) UUID() string {
172+
uuid := make([]byte, 16)
173+
r.rand.Read(uuid)
174+
uuid[6] = (uuid[6] & 0x0f) | 0x40
175+
uuid[8] = (uuid[8] & 0x3f) | 0x80
176+
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
177+
}
168178
func (r *FakerRandomizer) Name() string {
169179
return r.faker.Name()
170180
}
@@ -197,3 +207,4 @@ func (DeterministicRandomizer) IPv4Address() net.IP { return net.IPv4zero }
197207
func (DeterministicRandomizer) IPv6Address() net.IP { return net.IPv6zero }
198208
func (DeterministicRandomizer) URL() string { return "https://example.com/foo" }
199209
func (DeterministicRandomizer) Characters(n int) string { return strings.Repeat("a", n) }
210+
func (DeterministicRandomizer) UUID() string { return "550e8400-e29b-41d4-a716-446655440000" }

expr/types.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package expr
33
import (
44
"fmt"
55
"reflect"
6+
"sort"
67

78
"goa.design/goa/v3/eval"
89
)
@@ -591,8 +592,15 @@ func (m *Map) Example(r *ExampleGenerator) any {
591592
// which cannot be handled by json.Marshal.
592593
func (m *Map) MakeMap(raw map[any]any) any {
593594
ma := reflect.MakeMap(toReflectType(m))
594-
for key, value := range raw {
595-
ma.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value))
595+
keys := make([]any, 0, len(raw))
596+
for key := range raw {
597+
keys = append(keys, key)
598+
}
599+
sort.Slice(keys, func(i, j int) bool {
600+
return reflect.ValueOf(keys[i]).String() < reflect.ValueOf(keys[j]).String()
601+
})
602+
for _, key := range keys {
603+
ma.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(raw[key]))
596604
}
597605
return ma.Interface()
598606
}

go.mod

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ module goa.design/goa/v3
33
go 1.20
44

55
require (
6-
github.com/AnatolyRugalev/goregen v0.1.0
76
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
87
github.com/getkin/kin-openapi v0.124.0
98
github.com/go-chi/chi/v5 v5.0.12
@@ -15,7 +14,7 @@ require (
1514
github.com/stretchr/testify v1.9.0
1615
golang.org/x/text v0.15.0
1716
golang.org/x/tools v0.21.0
18-
google.golang.org/grpc v1.63.2
17+
google.golang.org/grpc v1.64.0
1918
google.golang.org/protobuf v1.34.1
2019
gopkg.in/yaml.v3 v3.0.1
2120
)
@@ -35,5 +34,5 @@ require (
3534
golang.org/x/net v0.25.0 // indirect
3635
golang.org/x/sync v0.7.0 // indirect
3736
golang.org/x/sys v0.20.0 // indirect
38-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
37+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
3938
)

go.sum

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE=
2-
github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI=
31
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
42
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
53
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -62,10 +60,10 @@ golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
6260
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
6361
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
6462
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
65-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
66-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
67-
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
68-
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
63+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI=
64+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
65+
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
66+
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
6967
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
7068
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
7169
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

grpc/middleware/canceler.go

+8-25
Original file line numberDiff line numberDiff line change
@@ -39,44 +39,27 @@ import (
3939
// ...
4040
func StreamCanceler(ctx context.Context) grpc.StreamServerInterceptor {
4141
var (
42-
cancels = map[*context.CancelFunc]struct{}{}
43-
cancelMu = new(sync.Mutex)
42+
cancels sync.Map
4443
canceling uint32
4544
)
4645

4746
go func() {
4847
<-ctx.Done()
4948
atomic.StoreUint32(&canceling, 1)
50-
cancelMu.Lock()
51-
defer cancelMu.Unlock()
52-
for cancel := range cancels {
49+
cancels.Range(func(key any, value any) bool {
50+
cancel := key.(*context.CancelFunc)
5351
(*cancel)()
54-
}
52+
return true
53+
})
5554
}()
5655
return grpc.StreamServerInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
5756
if atomic.LoadUint32(&canceling) == 1 {
5857
return status.Error(codes.Unavailable, "server is stopping")
5958
}
60-
var (
61-
cctx = ss.Context()
62-
cancel context.CancelFunc
63-
)
64-
cctx, cancel = context.WithCancel(cctx)
65-
66-
// add the cancel function
67-
cancelMu.Lock()
68-
cancels[&cancel] = struct{}{}
69-
cancelMu.Unlock()
70-
71-
// invoke rpc
59+
cctx, cancel := context.WithCancel(ss.Context())
60+
cancels.Store(&cancel, struct{}{})
7261
err := handler(srv, NewWrappedServerStream(cctx, ss))
73-
74-
// remove the cancel function
75-
cancelMu.Lock()
76-
delete(cancels, &cancel)
77-
cancelMu.Unlock()
78-
79-
// cleanup the WithCancel
62+
cancels.Delete(&cancel)
8063
cancel()
8164

8265
return err

grpc/middleware/canceler_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ func TestStreamCanceler(t *testing.T) {
5252
}()
5353

5454
if err := grpcm.StreamCanceler(ctx)(nil, c.stream, stream, c.handler); err != nil {
55-
t.Errorf("StreamCanceler error: %v", err)
55+
if err.Error() != "server is stopping" {
56+
t.Errorf("StreamCanceler error: %v", err)
57+
}
5658
}
5759
})
5860
}

0 commit comments

Comments
 (0)