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  }