/
connectconformance.go
489 lines (455 loc) · 15.1 KB
/
connectconformance.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
// Copyright 2023-2024 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package connectconformance
import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"sync"
"connectrpc.com/conformance/internal"
"connectrpc.com/conformance/internal/app/connectconformance/testsuites"
"connectrpc.com/conformance/internal/app/grpcclient"
"connectrpc.com/conformance/internal/app/grpcserver"
"connectrpc.com/conformance/internal/app/referenceclient"
"connectrpc.com/conformance/internal/app/referenceserver"
conformancev1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1"
"connectrpc.com/conformance/internal/tracer"
"golang.org/x/sync/semaphore"
)
// Flags are the config values for the test runner that may be provided via
// command-line flags and arguments.
type Flags struct {
ConfigFile string
RunPatterns []string
SkipPatterns []string
KnownFailingPatterns []string
KnownFlakyPatterns []string
Verbose bool
VeryVerbose bool
ClientCommand []string
ServerCommand []string
TestFiles []string
MaxServers uint
Parallelism uint
TLSCertFile string
TLSKeyFile string
ServerPort uint
ServerBind string
HTTPTrace bool
}
func Run(flags *Flags, logPrinter internal.Printer, errPrinter internal.Printer) (bool, error) {
var configData []byte
if flags.ConfigFile != "" {
var err error
if configData, err = os.ReadFile(flags.ConfigFile); err != nil {
return false, internal.EnsureFileName(err, flags.ConfigFile)
}
} else if flags.Verbose {
logPrinter.Printf("No config file provided. Using defaults.")
}
configCases, err := parseConfig(flags.ConfigFile, configData)
if err != nil {
return false, err
}
if flags.Verbose {
logPrinter.Printf("Computed %d config case permutations.", len(configCases))
}
knownFailing := parsePatterns(flags.KnownFailingPatterns)
if knownFailing == nil {
// treat as empty
knownFailing = &testTrie{}
}
knownFlaky := parsePatterns(flags.KnownFlakyPatterns)
if knownFlaky == nil {
// treat as empty
knownFlaky = &testTrie{}
}
runPatterns := parsePatterns(flags.RunPatterns)
skipPatterns := parsePatterns(flags.SkipPatterns)
var testSuiteData map[string][]byte
if len(flags.TestFiles) > 0 {
testSuiteData, err = testsuites.LoadTestSuitesFromFiles(flags.TestFiles)
if err != nil {
return false, fmt.Errorf("failed to load test suite data: %w", err)
}
} else {
testSuiteData, err = testsuites.LoadTestSuites()
if err != nil {
return false, fmt.Errorf("failed to load embedded test suite data: %w", err)
}
}
allSuites, err := parseTestSuites(testSuiteData)
if err != nil {
return false, fmt.Errorf("embedded test suite: %w", err)
}
if flags.Verbose {
var numCases int
for _, suite := range allSuites {
numCases += len(suite.TestCases)
}
logPrinter.Printf("Loaded %d test suite(s), %d test case template(s).", len(allSuites), numCases)
}
results, err := run(configCases, knownFailing, knownFlaky, runPatterns, skipPatterns, allSuites, logPrinter, errPrinter, flags)
if results == nil {
return false, err
}
if err != nil {
errPrinter.Printf("%v", err)
}
return results.report(logPrinter) && err == nil, nil
}
func run( //nolint:gocyclo
configCases []configCase,
knownFailing *testTrie,
knownFlaky *testTrie,
run *testTrie,
skip *testTrie,
allSuites map[string]*conformancev1.TestSuite,
logPrinter internal.Printer,
errPrinter internal.Printer,
flags *Flags,
) (*testResults, error) {
mode := conformancev1.TestSuite_TEST_MODE_UNSPECIFIED
useReferenceClient := len(flags.ClientCommand) == 0
useReferenceServer := len(flags.ServerCommand) == 0
switch {
case useReferenceServer && !useReferenceClient:
// Client mode uses a reference server to test a given client
mode = conformancev1.TestSuite_TEST_MODE_CLIENT
case useReferenceClient && !useReferenceServer:
// Server mode uses a reference client to test a given server
mode = conformancev1.TestSuite_TEST_MODE_SERVER
default:
// Otherwise, leave mode as "unspecified" so we'll include
// neither client-specific nor server-specific cases.
}
testCaseLib, err := newTestCaseLibrary(allSuites, configCases, mode)
if err != nil {
return nil, err
}
svrInstances := serverInstancesSlice(testCaseLib, flags.Verbose)
// Calculate all permutations of test cases that will be run, including gRPC tests
allPermutations := testCaseLib.allPermutations(useReferenceClient, useReferenceServer)
// Validate keys in knownFailing, runPatterns, and noRunPatterns, to
// make sure they match actual test names (to prevent accidental typos
// and inadvertently ignored entries)
if knownFailing.length() > 0 {
matched, err := tryMatchPatterns("known failing", knownFailing, allPermutations)
if err != nil {
return nil, err
}
if flags.Verbose {
logPrinter.Printf("Loaded %d known failing test case pattern(s) that match %d test case permutation(s).",
knownFailing.length(), matched)
}
}
if knownFlaky.length() > 0 {
matched, err := tryMatchPatterns("known flaky", knownFlaky, allPermutations)
if err != nil {
return nil, err
}
if flags.Verbose {
logPrinter.Printf("Loaded %d known flaky test case pattern(s) that match %d test case permutation(s).",
knownFlaky.length(), matched)
}
}
if run != nil {
if _, err := tryMatchPatterns("run patterns", run, allPermutations); err != nil {
return nil, err
}
}
if skip != nil {
if _, err := tryMatchPatterns("no-run patterns", skip, allPermutations); err != nil {
return nil, err
}
}
// we don't allow ambiguity whether a file is known to fail vs known to be flaky
if knownFailing.length() > 0 && knownFlaky.length() > 0 {
var conflicts []string
for _, testCase := range allPermutations {
name := testCase.Request.TestName
if knownFailing.matchPattern(name) && knownFlaky.matchPattern(name) {
conflicts = append(conflicts, name)
}
}
if len(conflicts) > 0 {
sort.Strings(conflicts)
return nil, fmt.Errorf("known failing and known flaky configs are ambiguous as some test cases are matched as both\n:%v", strings.Join(conflicts, "\n"))
}
}
filter := newFilter(run, skip)
var filteredTestCount int
type serverConfig struct {
serverInstance
isGrpcClient, isGrpcServer bool
}
allServerConfigs, filteredServerConfigs := map[serverConfig]struct{}{}, map[serverConfig]struct{}{}
for _, testCase := range allPermutations {
svrConfig := serverConfig{
serverInstance: serverInstance{
protocol: testCase.Request.Protocol,
httpVersion: testCase.Request.HttpVersion,
useTLS: len(testCase.Request.ServerTlsCert) > 0,
useTLSClientCerts: testCase.Request.ClientTlsCreds != nil,
},
isGrpcClient: strings.Contains(testCase.Request.TestName, grpcImplMarker) ||
strings.Contains(testCase.Request.TestName, grpcClientImplMarker),
isGrpcServer: strings.Contains(testCase.Request.TestName, grpcImplMarker) ||
strings.Contains(testCase.Request.TestName, grpcServerImplMarker),
}
allServerConfigs[svrConfig] = struct{}{}
if filter.accept(testCase) {
filteredTestCount++
filteredServerConfigs[svrConfig] = struct{}{}
}
}
if flags.Verbose {
logPrinter.Printf("Computed %d test case permutation(s) across %d server configuration(s).",
len(allPermutations), len(allServerConfigs))
if filteredTestCount != len(allPermutations) {
logPrinter.Printf("Filtered tests to %d test case permutation(s) across %d server configuration(s).",
filteredTestCount, len(filteredServerConfigs))
}
}
var serverCreds, clientCreds *conformancev1.TLSCreds
for svrInstance := range testCaseLib.casesByServer {
if svrInstance.useTLS && serverCreds == nil {
serverCertBytes, serverKeyBytes, err := internal.NewServerCert()
if err != nil {
return nil, fmt.Errorf("failed to generate server certificate: %w", err)
}
serverCreds = &conformancev1.TLSCreds{
Cert: serverCertBytes,
Key: serverKeyBytes,
}
}
if svrInstance.useTLSClientCerts {
clientCertBytes, clientKeyBytes, err := internal.NewClientCert()
if err != nil {
return nil, fmt.Errorf("failed to generate client certificate: %w", err)
}
clientCreds = &conformancev1.TLSCreds{
Cert: clientCertBytes,
Key: clientKeyBytes,
}
break
}
}
var trace *tracer.Tracer
if flags.HTTPTrace {
trace = &tracer.Tracer{}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var clients []processInfo
if useReferenceClient {
clients = []processInfo{
{
name: "reference client",
start: runInProcess([]string{
"reference-client",
"-p", strconv.Itoa(int(flags.Parallelism)),
}, func(ctx context.Context, args []string, inReader io.ReadCloser, outWriter, errWriter io.WriteCloser) error {
return referenceclient.RunInReferenceMode(ctx, args, inReader, outWriter, errWriter, trace)
}),
isReferenceImpl: true,
},
{
name: "reference client (grpc)",
start: runInProcess([]string{
"grpc-reference-client",
"-p", strconv.Itoa(int(flags.Parallelism)),
}, func(ctx context.Context, args []string, inReader io.ReadCloser, outWriter, errWriter io.WriteCloser) error {
return grpcclient.RunWithTrace(ctx, args, inReader, outWriter, errWriter, trace)
}),
isGrpcImpl: true,
},
}
} else {
clients = []processInfo{
{
start: runCommand(flags.ClientCommand),
},
}
}
results := newResults(filteredTestCount, knownFailing, knownFlaky, trace)
for _, clientInfo := range clients {
clientProcess, err := runClient(ctx, clientInfo.start)
if err != nil {
return nil, fmt.Errorf("error starting client: %w", err)
}
defer clientProcess.stop()
var servers []processInfo
if useReferenceServer {
servers = []processInfo{
{
name: "reference server",
start: runInProcess([]string{
"reference-server",
"-port", strconv.FormatUint(uint64(flags.ServerPort), 10),
"-bind", flags.ServerBind,
"-cert", flags.TLSCertFile,
"-key", flags.TLSKeyFile,
}, func(ctx context.Context, args []string, inReader io.ReadCloser, outWriter, errWriter io.WriteCloser) error {
return referenceserver.RunInReferenceMode(ctx, args, inReader, outWriter, errWriter, trace)
}),
isReferenceImpl: true,
},
{
name: "reference server (grpc)",
start: runInProcess([]string{
"grpc-reference-server",
"-port", strconv.FormatUint(uint64(flags.ServerPort), 10),
"-bind", flags.ServerBind,
}, func(ctx context.Context, args []string, inReader io.ReadCloser, outWriter, errWriter io.WriteCloser) error {
return grpcserver.RunWithTrace(ctx, args, inReader, outWriter, errWriter, trace)
}),
isGrpcImpl: true,
},
}
} else {
servers = []processInfo{
{
start: runCommand(flags.ServerCommand),
},
}
}
err = func() error {
var wg sync.WaitGroup
defer wg.Wait()
sema := semaphore.NewWeighted(int64(flags.MaxServers))
for _, serverInfo := range servers {
for _, svrInstance := range svrInstances {
testCases := testCaseLib.casesByServer[svrInstance]
testCases = testCaseLib.filterGRPCImplTestCases(testCases, clientInfo.isGrpcImpl, serverInfo.isGrpcImpl)
testCases = filter.apply(testCases)
if len(testCases) == 0 {
continue
}
if err := sema.Acquire(ctx, 1); err != nil {
return err
}
// Double-check that client is still running before spawning a server process.
if !clientProcess.isRunning() {
err := clientProcess.waitForResponses()
if err == nil {
err = errors.New("client process unexpectedly stopped")
}
return err
}
if flags.Verbose {
var with string
switch {
case clientInfo.name != "" && serverInfo.name != "":
with = clientInfo.name + " and " + serverInfo.name
case clientInfo.name != "":
with = clientInfo.name
case serverInfo.name != "":
with = serverInfo.name
}
logTestCaseInfo(with, svrInstance, len(testCases), logPrinter)
}
wg.Add(1)
go func(ctx context.Context, clientInfo processInfo, serverInfo processInfo, svrInstance serverInstance) {
defer wg.Done()
defer sema.Release(1)
runTestCasesForServer(
ctx,
clientInfo.isReferenceImpl,
serverInfo.isReferenceImpl,
svrInstance,
testCases,
serverCreds,
clientCreds,
serverInfo.start,
logPrinter,
errPrinter,
results,
clientProcess,
trace,
flags.VeryVerbose,
)
}(ctx, clientInfo, serverInfo, svrInstance)
}
}
return nil
}()
if err != nil {
return results, err
}
clientProcess.closeSend()
if err := clientProcess.waitForResponses(); err != nil {
return results, err
}
}
return results, nil
}
func serverInstancesSlice(testCaseLib *testCaseLibrary, sorted bool) []serverInstance {
svrInstances := make([]serverInstance, 0, len(testCaseLib.casesByServer))
for svrInstance := range testCaseLib.casesByServer {
svrInstances = append(svrInstances, svrInstance)
}
if !sorted {
return svrInstances
}
sort.Slice(svrInstances, func(i, j int) bool { //nolint:varnamelen
if svrInstances[i].httpVersion != svrInstances[j].httpVersion {
return svrInstances[i].httpVersion < svrInstances[j].httpVersion
}
if svrInstances[i].protocol != svrInstances[j].protocol {
return svrInstances[i].protocol < svrInstances[j].protocol
}
if svrInstances[i].useTLS != svrInstances[j].useTLS {
return !svrInstances[i].useTLS
}
return !svrInstances[i].useTLSClientCerts || svrInstances[j].useTLSClientCerts
})
return svrInstances
}
func logTestCaseInfo(with string, svrInstance serverInstance, numCases int, logPrinter internal.Printer) {
var tlsMode string
switch {
case !svrInstance.useTLS:
tlsMode = "false"
case svrInstance.useTLS && svrInstance.useTLSClientCerts:
tlsMode = "true (with client certs)"
default:
tlsMode = "true"
}
logPrinter.Printf("Running %d tests with %s for server config {%s, %s, TLS:%s}...",
numCases, with, svrInstance.httpVersion, svrInstance.protocol, tlsMode)
}
func tryMatchPatterns(what string, patterns *testTrie, testCases []*conformancev1.TestCase) (int, error) {
var matchCount int
for _, tc := range testCases {
if patterns.matchPattern(tc.Request.TestName) {
matchCount++
}
}
unmatched := patterns.allUnmatched()
if len(unmatched) == 0 {
return matchCount, nil
}
unmatchedSlice := make([]string, 0, len(unmatched))
for name := range unmatched {
unmatchedSlice = append(unmatchedSlice, name)
}
sort.Strings(unmatchedSlice)
return matchCount, fmt.Errorf("%s: unmatched and possibly invalid patterns:\n%v", what, strings.Join(unmatchedSlice, "\n"))
}