github.com/Jeffail/benthos/v3@v3.65.0/internal/integration/stream_test_helpers.go (about) 1 package integration 2 3 import ( 4 "bytes" 5 "context" 6 "flag" 7 "fmt" 8 "net" 9 "os" 10 "regexp" 11 "strconv" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/Jeffail/benthos/v3/lib/config" 18 "github.com/Jeffail/benthos/v3/lib/input" 19 "github.com/Jeffail/benthos/v3/lib/log" 20 "github.com/Jeffail/benthos/v3/lib/manager" 21 "github.com/Jeffail/benthos/v3/lib/message" 22 "github.com/Jeffail/benthos/v3/lib/metrics" 23 "github.com/Jeffail/benthos/v3/lib/output" 24 "github.com/Jeffail/benthos/v3/lib/response" 25 "github.com/Jeffail/benthos/v3/lib/types" 26 27 "github.com/gofrs/uuid" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 "gopkg.in/yaml.v3" 31 ) 32 33 // CheckSkip marks a test to be skipped unless the integration test has been 34 // specifically requested using the -run flag. 35 func CheckSkip(t *testing.T) { 36 if m := flag.Lookup("test.run").Value.String(); m == "" || regexp.MustCompile(strings.Split(m, "/")[0]).FindString(t.Name()) == "" { 37 t.Skip("Skipping as execution was not requested explicitly using go test -run ^TestIntegration$") 38 } 39 } 40 41 // GetFreePort attempts to get a free port. This involves creating a bind and 42 // then immediately dropping it and so it's ever so slightly flakey. 43 func GetFreePort() (int, error) { 44 addr, err := net.ResolveTCPAddr("tcp", "localhost:0") 45 if err != nil { 46 return 0, err 47 } 48 49 listener, err := net.ListenTCP("tcp", addr) 50 if err != nil { 51 return 0, err 52 } 53 defer listener.Close() 54 return listener.Addr().(*net.TCPAddr).Port, nil 55 } 56 57 // StreamTestConfigVars defines variables that will be accessed by test 58 // definitions when generating components through the config template. The main 59 // value is the id, which is generated for each test for isolation, and the port 60 // which is injected into the config template. 61 type StreamTestConfigVars struct { 62 // A unique identifier for separating this test configuration from others. 63 // Usually used to access a different topic, consumer group, directory, etc. 64 ID string 65 66 // A port to use in connector URLs. Allowing tests to override this 67 // potentially enables tests that check for faulty connections by bridging. 68 port string 69 70 // A second port to use in secondary connector URLs. 71 portTwo string 72 73 // A third port to use in tertiary connector URLs. 74 portThree string 75 76 // A fourth port to use in quarternary connector URLs. 77 portFour string 78 79 // Used by batching testers to check the input honours batching fields. 80 InputBatchCount int 81 82 // Used by batching testers to check the output honours batching fields. 83 OutputBatchCount int 84 85 // Used by metadata filter tests to check that filters work. 86 OutputMetaExcludePrefix string 87 88 // Used by testers to check the max in flight option of outputs. 89 MaxInFlight int 90 91 // Generic variables. 92 Var1 string 93 Var2 string 94 Var3 string 95 Var4 string 96 } 97 98 // StreamPreTestFn is an optional closure to be called before tests are run, 99 // this is an opportunity to mutate test config variables and mess with the 100 // environment. 101 type StreamPreTestFn func(t testing.TB, ctx context.Context, testID string, vars *StreamTestConfigVars) 102 103 type streamTestEnvironment struct { 104 configTemplate string 105 configVars StreamTestConfigVars 106 107 preTest StreamPreTestFn 108 109 timeout time.Duration 110 ctx context.Context 111 log log.Modular 112 stats metrics.Type 113 mgr types.Manager 114 115 allowDuplicateMessages bool 116 117 // Ugly work arounds for slow connectors. 118 sleepAfterInput time.Duration 119 sleepAfterOutput time.Duration 120 } 121 122 func getFreePort() (int, error) { 123 addr, err := net.ResolveTCPAddr("tcp", "localhost:0") 124 if err != nil { 125 return 0, err 126 } 127 128 listener, err := net.ListenTCP("tcp", addr) 129 if err != nil { 130 return 0, err 131 } 132 defer listener.Close() 133 return listener.Addr().(*net.TCPAddr).Port, nil 134 } 135 136 func newStreamTestEnvironment(t testing.TB, confTemplate string) streamTestEnvironment { 137 t.Helper() 138 139 u4, err := uuid.NewV4() 140 require.NoError(t, err) 141 142 return streamTestEnvironment{ 143 configTemplate: confTemplate, 144 configVars: StreamTestConfigVars{ 145 ID: u4.String(), 146 MaxInFlight: 1, 147 }, 148 timeout: time.Second * 90, 149 ctx: context.Background(), 150 log: log.Noop(), 151 stats: metrics.Noop(), 152 } 153 } 154 155 func (e streamTestEnvironment) RenderConfig() string { 156 return strings.NewReplacer( 157 "$ID", e.configVars.ID, 158 "$PORT_TWO", e.configVars.portTwo, 159 "$PORT_THREE", e.configVars.portThree, 160 "$PORT_FOUR", e.configVars.portFour, 161 "$PORT", e.configVars.port, 162 "$VAR1", e.configVars.Var1, 163 "$VAR2", e.configVars.Var2, 164 "$VAR3", e.configVars.Var3, 165 "$VAR4", e.configVars.Var4, 166 "$INPUT_BATCH_COUNT", strconv.Itoa(e.configVars.InputBatchCount), 167 "$OUTPUT_BATCH_COUNT", strconv.Itoa(e.configVars.OutputBatchCount), 168 "$OUTPUT_META_EXCLUDE_PREFIX", e.configVars.OutputMetaExcludePrefix, 169 "$MAX_IN_FLIGHT", strconv.Itoa(e.configVars.MaxInFlight), 170 ).Replace(e.configTemplate) 171 } 172 173 //------------------------------------------------------------------------------ 174 175 // StreamTestOptFunc is an opt func for customizing the behaviour of stream 176 // tests, these are useful for things that are integration environment specific, 177 // such as the port of the service being interacted with. 178 type StreamTestOptFunc func(*streamTestEnvironment) 179 180 // StreamTestOptTimeout describes an optional timeout spanning the entirety of 181 // the test suite. 182 func StreamTestOptTimeout(timeout time.Duration) StreamTestOptFunc { 183 return func(env *streamTestEnvironment) { 184 env.timeout = timeout 185 } 186 } 187 188 // StreamTestOptAllowDupes specifies across all stream tests that in this 189 // environment we can expect duplicates and these are not considered errors. 190 func StreamTestOptAllowDupes() StreamTestOptFunc { 191 return func(env *streamTestEnvironment) { 192 env.allowDuplicateMessages = true 193 } 194 } 195 196 // StreamTestOptMaxInFlight configures a maximum inflight (to be injected into 197 // the config template) for all tests. 198 func StreamTestOptMaxInFlight(n int) StreamTestOptFunc { 199 return func(env *streamTestEnvironment) { 200 env.configVars.MaxInFlight = n 201 } 202 } 203 204 // StreamTestOptLogging allows components to log with the given log level. This 205 // is useful for diagnosing issues. 206 func StreamTestOptLogging(level string) StreamTestOptFunc { 207 return func(env *streamTestEnvironment) { 208 logConf := log.NewConfig() 209 logConf.LogLevel = level 210 env.log = log.New(os.Stdout, logConf) 211 } 212 } 213 214 // StreamTestOptPort defines the port of the integration service. 215 func StreamTestOptPort(port string) StreamTestOptFunc { 216 return func(env *streamTestEnvironment) { 217 env.configVars.port = port 218 } 219 } 220 221 // StreamTestOptPortTwo defines a secondary port of the integration service. 222 func StreamTestOptPortTwo(portTwo string) StreamTestOptFunc { 223 return func(env *streamTestEnvironment) { 224 env.configVars.portTwo = portTwo 225 } 226 } 227 228 // StreamTestOptVarOne sets an arbitrary variable for the test that can be 229 // injected into templated configs. 230 func StreamTestOptVarOne(v string) StreamTestOptFunc { 231 return func(env *streamTestEnvironment) { 232 env.configVars.Var1 = v 233 } 234 } 235 236 // StreamTestOptVarTwo sets a second arbitrary variable for the test that can be 237 // injected into templated configs. 238 func StreamTestOptVarTwo(v string) StreamTestOptFunc { 239 return func(env *streamTestEnvironment) { 240 env.configVars.Var2 = v 241 } 242 } 243 244 // StreamTestOptVarThree sets a third arbitrary variable for the test that can 245 // be injected into templated configs. 246 func StreamTestOptVarThree(v string) StreamTestOptFunc { 247 return func(env *streamTestEnvironment) { 248 env.configVars.Var3 = v 249 } 250 } 251 252 // StreamTestOptSleepAfterInput adds a sleep to tests after the input has been 253 // created. 254 func StreamTestOptSleepAfterInput(t time.Duration) StreamTestOptFunc { 255 return func(env *streamTestEnvironment) { 256 env.sleepAfterInput = t 257 } 258 } 259 260 // StreamTestOptSleepAfterOutput adds a sleep to tests after the output has been 261 // created. 262 func StreamTestOptSleepAfterOutput(t time.Duration) StreamTestOptFunc { 263 return func(env *streamTestEnvironment) { 264 env.sleepAfterOutput = t 265 } 266 } 267 268 // StreamTestOptPreTest adds a closure to be executed before each test. 269 func StreamTestOptPreTest(fn StreamPreTestFn) StreamTestOptFunc { 270 return func(env *streamTestEnvironment) { 271 env.preTest = fn 272 } 273 } 274 275 //------------------------------------------------------------------------------ 276 277 type streamTestDefinitionFn func(*testing.T, *streamTestEnvironment) 278 279 // StreamTestDefinition encompasses a unit test to be executed against an 280 // integration environment. These tests are generic and can be run against any 281 // configuration containing an input and an output that are connected. 282 type StreamTestDefinition struct { 283 fn func(*testing.T, *streamTestEnvironment) 284 } 285 286 // StreamTestList is a list of stream definitions that can be run with a single 287 // template and function args. 288 type StreamTestList []StreamTestDefinition 289 290 // StreamTests creates a list of tests from variadic arguments. 291 func StreamTests(tests ...StreamTestDefinition) StreamTestList { 292 return tests 293 } 294 295 // Run all the tests against a config template. Tests are run in parallel. 296 func (i StreamTestList) Run(t *testing.T, configTemplate string, opts ...StreamTestOptFunc) { 297 envs := make([]streamTestEnvironment, len(i)) 298 299 wg := sync.WaitGroup{} 300 for j := range i { 301 envs[j] = newStreamTestEnvironment(t, configTemplate) 302 for _, opt := range opts { 303 opt(&envs[j]) 304 } 305 306 timeout := envs[j].timeout 307 if deadline, ok := t.Deadline(); ok { 308 timeout = time.Until(deadline) - (time.Second * 5) 309 } 310 311 var done func() 312 envs[j].ctx, done = context.WithTimeout(envs[j].ctx, timeout) 313 t.Cleanup(done) 314 315 if envs[j].preTest != nil { 316 wg.Add(1) 317 env := &envs[j] 318 go func() { 319 defer wg.Done() 320 env.preTest(t, env.ctx, env.configVars.ID, &env.configVars) 321 }() 322 } 323 } 324 wg.Wait() 325 326 for j, test := range i { 327 if envs[j].configVars.port == "" { 328 p, err := getFreePort() 329 if err != nil { 330 t.Fatal(err) 331 } 332 envs[j].configVars.port = strconv.Itoa(p) 333 } 334 test.fn(t, &envs[j]) 335 } 336 } 337 338 // RunSequentially executes all the tests against a config template 339 // sequentially. 340 func (i StreamTestList) RunSequentially(t *testing.T, configTemplate string, opts ...StreamTestOptFunc) { 341 for _, test := range i { 342 env := newStreamTestEnvironment(t, configTemplate) 343 for _, opt := range opts { 344 opt(&env) 345 } 346 347 timeout := env.timeout 348 if deadline, ok := t.Deadline(); ok { 349 timeout = time.Until(deadline) - (time.Second * 5) 350 } 351 352 var done func() 353 env.ctx, done = context.WithTimeout(env.ctx, timeout) 354 t.Cleanup(done) 355 356 if env.preTest != nil { 357 env.preTest(t, env.ctx, env.configVars.ID, &env.configVars) 358 } 359 t.Run("seq", func(t *testing.T) { 360 test.fn(t, &env) 361 }) 362 } 363 } 364 365 //------------------------------------------------------------------------------ 366 367 func namedStreamTest(name string, test streamTestDefinitionFn) StreamTestDefinition { 368 return StreamTestDefinition{ 369 fn: func(t *testing.T, env *streamTestEnvironment) { 370 t.Run(name, func(t *testing.T) { 371 test(t, env) 372 }) 373 }, 374 } 375 } 376 377 //------------------------------------------------------------------------------ 378 379 type streamBenchDefinitionFn func(*testing.B, *streamTestEnvironment) 380 381 // StreamBenchDefinition encompasses a benchmark to be executed against an 382 // integration environment. These tests are generic and can be run against any 383 // configuration containing an input and an output that are connected. 384 type StreamBenchDefinition struct { 385 fn streamBenchDefinitionFn 386 } 387 388 // StreamBenchList is a list of stream benchmark definitions that can be run 389 // with a single template and function args. 390 type StreamBenchList []StreamBenchDefinition 391 392 // StreamBenchs creates a list of benchmarks from variadic arguments. 393 func StreamBenchs(tests ...StreamBenchDefinition) StreamBenchList { 394 return tests 395 } 396 397 // Run the benchmarks against a config template. 398 func (i StreamBenchList) Run(b *testing.B, configTemplate string, opts ...StreamTestOptFunc) { 399 for _, bench := range i { 400 env := newStreamTestEnvironment(b, configTemplate) 401 for _, opt := range opts { 402 opt(&env) 403 } 404 405 if env.preTest != nil { 406 env.preTest(b, env.ctx, env.configVars.ID, &env.configVars) 407 } 408 bench.fn(b, &env) 409 } 410 } 411 412 func namedBench(name string, test streamBenchDefinitionFn) StreamBenchDefinition { 413 return StreamBenchDefinition{ 414 fn: func(b *testing.B, env *streamTestEnvironment) { 415 b.Run(name, func(b *testing.B) { 416 test(b, env) 417 }) 418 }, 419 } 420 } 421 422 //------------------------------------------------------------------------------ 423 424 func initConnectors( 425 t testing.TB, 426 trans <-chan types.Transaction, 427 env *streamTestEnvironment, 428 ) (types.Input, types.Output) { 429 t.Helper() 430 431 out := initOutput(t, trans, env) 432 in := initInput(t, env) 433 return in, out 434 } 435 436 func initInput(t testing.TB, env *streamTestEnvironment) types.Input { 437 t.Helper() 438 439 confBytes := []byte(env.RenderConfig()) 440 441 s := config.New() 442 dec := yaml.NewDecoder(bytes.NewReader(confBytes)) 443 dec.KnownFields(true) 444 require.NoError(t, dec.Decode(&s)) 445 446 lints, err := config.Lint(confBytes, s) 447 require.NoError(t, err) 448 assert.Empty(t, lints) 449 450 if env.mgr == nil { 451 env.mgr, err = manager.NewV2(s.ResourceConfig, nil, env.log, env.stats) 452 require.NoError(t, err) 453 } 454 455 input, err := input.New(s.Input, env.mgr, env.log, env.stats) 456 require.NoError(t, err) 457 458 if env.sleepAfterInput > 0 { 459 time.Sleep(env.sleepAfterInput) 460 } 461 462 return input 463 } 464 465 func initOutput(t testing.TB, trans <-chan types.Transaction, env *streamTestEnvironment) types.Output { 466 t.Helper() 467 468 confBytes := []byte(env.RenderConfig()) 469 470 s := config.New() 471 dec := yaml.NewDecoder(bytes.NewReader(confBytes)) 472 dec.KnownFields(true) 473 require.NoError(t, dec.Decode(&s)) 474 475 lints, err := config.Lint(confBytes, s) 476 require.NoError(t, err) 477 assert.Empty(t, lints) 478 479 if env.mgr == nil { 480 env.mgr, err = manager.NewV2(s.ResourceConfig, nil, env.log, env.stats) 481 require.NoError(t, err) 482 } 483 484 output, err := output.New(s.Output, env.mgr, env.log, env.stats) 485 require.NoError(t, err) 486 487 require.NoError(t, output.Consume(trans)) 488 489 require.Error(t, output.WaitForClose(time.Millisecond*100)) 490 if env.sleepAfterOutput > 0 { 491 time.Sleep(env.sleepAfterOutput) 492 } 493 494 return output 495 } 496 497 func closeConnectors(t testing.TB, input types.Input, output types.Output) { 498 if output != nil { 499 output.CloseAsync() 500 require.NoError(t, output.WaitForClose(time.Second*10)) 501 } 502 if input != nil { 503 input.CloseAsync() 504 require.NoError(t, input.WaitForClose(time.Second*10)) 505 } 506 } 507 508 func sendMessage( 509 ctx context.Context, 510 t testing.TB, 511 tranChan chan types.Transaction, 512 content string, 513 metadata ...string, 514 ) error { 515 t.Helper() 516 517 p := message.NewPart([]byte(content)) 518 for i := 0; i < len(metadata); i += 2 { 519 p.Metadata().Set(metadata[i], metadata[i+1]) 520 } 521 msg := message.New(nil) 522 msg.Append(p) 523 524 resChan := make(chan types.Response) 525 526 select { 527 case tranChan <- types.NewTransaction(msg, resChan): 528 case <-ctx.Done(): 529 t.Fatal("timed out on send") 530 } 531 532 select { 533 case res := <-resChan: 534 return res.Error() 535 case <-ctx.Done(): 536 } 537 t.Fatal("timed out on response") 538 return nil 539 } 540 541 func sendBatch( 542 ctx context.Context, 543 t testing.TB, 544 tranChan chan types.Transaction, 545 content []string, 546 ) error { 547 t.Helper() 548 549 msg := message.New(nil) 550 for _, payload := range content { 551 msg.Append(message.NewPart([]byte(payload))) 552 } 553 554 resChan := make(chan types.Response) 555 556 select { 557 case tranChan <- types.NewTransaction(msg, resChan): 558 case <-ctx.Done(): 559 t.Fatal("timed out on send") 560 } 561 562 select { 563 case res := <-resChan: 564 return res.Error() 565 case <-ctx.Done(): 566 } 567 568 t.Fatal("timed out on response") 569 return nil 570 } 571 572 func receiveMessage( 573 ctx context.Context, 574 t testing.TB, 575 tranChan <-chan types.Transaction, 576 err error, 577 ) types.Part { 578 t.Helper() 579 580 b, resChan := receiveMessageNoRes(ctx, t, tranChan) 581 sendResponse(ctx, t, resChan, err) 582 return b 583 } 584 585 func sendResponse(ctx context.Context, t testing.TB, resChan chan<- types.Response, err error) { 586 var res types.Response = response.NewAck() 587 if err != nil { 588 res = response.NewError(err) 589 } 590 591 select { 592 case resChan <- res: 593 case <-ctx.Done(): 594 t.Fatal("timed out on response") 595 } 596 } 597 598 // nolint:gocritic // Ignore unnamedResult false positive 599 func receiveMessageNoRes(ctx context.Context, t testing.TB, tranChan <-chan types.Transaction) (types.Part, chan<- types.Response) { 600 t.Helper() 601 602 var tran types.Transaction 603 var open bool 604 select { 605 case tran, open = <-tranChan: 606 case <-ctx.Done(): 607 t.Fatal("timed out on receive") 608 } 609 610 require.True(t, open) 611 require.Equal(t, tran.Payload.Len(), 1) 612 613 return tran.Payload.Get(0), tran.ResponseChan 614 } 615 616 func messageMatch(t testing.TB, p types.Part, content string, metadata ...string) { 617 t.Helper() 618 619 assert.Equal(t, content, string(p.Get())) 620 621 allMetadata := map[string]string{} 622 p.Metadata().Iter(func(k, v string) error { 623 allMetadata[k] = v 624 return nil 625 }) 626 627 for i := 0; i < len(metadata); i += 2 { 628 assert.Equal(t, metadata[i+1], p.Metadata().Get(metadata[i]), fmt.Sprintf("metadata: %v", allMetadata)) 629 } 630 } 631 632 func messageInSet(t testing.TB, pop, allowDupes bool, p types.Part, set map[string][]string) { 633 t.Helper() 634 635 metadata, exists := set[string(p.Get())] 636 if allowDupes && !exists { 637 return 638 } 639 require.True(t, exists, "in set: %v, set: %v", string(p.Get()), set) 640 641 for i := 0; i < len(metadata); i += 2 { 642 assert.Equal(t, metadata[i+1], p.Metadata().Get(metadata[i])) 643 } 644 645 if pop { 646 delete(set, string(p.Get())) 647 } 648 }