github.com/Jeffail/benthos/v3@v3.65.0/lib/processor/branch_test.go (about)

     1  package processor
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/Jeffail/benthos/v3/lib/log"
     8  	"github.com/Jeffail/benthos/v3/lib/message"
     9  	"github.com/Jeffail/benthos/v3/lib/metrics"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  func TestBranchBasic(t *testing.T) {
    15  	type mockMsg struct {
    16  		content string
    17  		meta    map[string]string
    18  	}
    19  	msg := func(content string, meta ...string) mockMsg {
    20  		t.Helper()
    21  		m := mockMsg{
    22  			content: content,
    23  			meta:    map[string]string{},
    24  		}
    25  		for i, v := range meta {
    26  			if i%2 == 1 {
    27  				m.meta[meta[i-1]] = v
    28  			}
    29  		}
    30  		return m
    31  	}
    32  
    33  	tests := map[string]struct {
    34  		requestMap   string
    35  		processorMap string
    36  		resultMap    string
    37  		input        []mockMsg
    38  		output       []mockMsg
    39  	}{
    40  		"empty request mapping": {
    41  			requestMap:   "",
    42  			processorMap: "root.nested = this",
    43  			resultMap:    "root.result = this.nested",
    44  			input: []mockMsg{
    45  				msg(`{"value":"foobar"}`),
    46  			},
    47  			output: []mockMsg{
    48  				msg(`{"result":{"value":"foobar"},"value":"foobar"}`),
    49  			},
    50  		},
    51  		"empty result mapping": {
    52  			requestMap:   "root.nested = this",
    53  			processorMap: "root = this",
    54  			resultMap:    "",
    55  			input: []mockMsg{
    56  				msg(`{"value":"foobar"}`),
    57  			},
    58  			output: []mockMsg{
    59  				msg(`{"value":"foobar"}`),
    60  			},
    61  		},
    62  		"copy metadata over only": {
    63  			requestMap:   `meta foo = meta("foo")`,
    64  			processorMap: `meta foo = meta("foo") + " and this"`,
    65  			resultMap:    `meta new_foo = meta("foo")`,
    66  			input: []mockMsg{
    67  				msg(
    68  					`{"value":"foobar"}`,
    69  					"foo", "bar",
    70  				),
    71  			},
    72  			output: []mockMsg{
    73  				msg(
    74  					`{"value":"foobar"}`,
    75  					"foo", "bar",
    76  					"new_foo", "bar and this",
    77  				),
    78  			},
    79  		},
    80  		"do not carry error into branch": {
    81  			requestMap: `root = this`,
    82  			processorMap: `root = this
    83  				root.name_upper = this.name.uppercase()`,
    84  			resultMap: `root.result = if this.failme.bool(false) {
    85  					throw("this is a branch error")
    86  				} else {
    87  					this.name_upper
    88  				}`,
    89  			input: []mockMsg{
    90  				msg(
    91  					`{"id":0,"name":"first"}`,
    92  					FailFlagKey, "this is a pre-existing failure",
    93  				),
    94  				msg(`{"failme":true,"id":1,"name":"second"}`),
    95  				msg(
    96  					`{"failme":true,"id":2,"name":"third"}`,
    97  					FailFlagKey, "this is a pre-existing failure",
    98  				),
    99  			},
   100  			output: []mockMsg{
   101  				msg(
   102  					`{"id":0,"name":"first","result":"FIRST"}`,
   103  					FailFlagKey, "this is a pre-existing failure",
   104  				),
   105  				msg(
   106  					`{"failme":true,"id":1,"name":"second"}`,
   107  					FailFlagKey, "result mapping failed: failed assignment (line 1): this is a branch error",
   108  				),
   109  				msg(
   110  					`{"failme":true,"id":2,"name":"third"}`,
   111  					FailFlagKey, "result mapping failed: failed assignment (line 1): this is a branch error",
   112  				),
   113  			},
   114  		},
   115  		"map error into branch": {
   116  			requestMap:   `root.err = error()`,
   117  			processorMap: `root.err = this.err.uppercase()`,
   118  			resultMap:    `root.result_err = this.err`,
   119  			input: []mockMsg{
   120  				msg(
   121  					`{"id":0,"name":"first"}`,
   122  					FailFlagKey, "this is a pre-existing failure",
   123  				),
   124  				msg(`{"id":1,"name":"second"}`),
   125  			},
   126  			output: []mockMsg{
   127  				msg(
   128  					`{"id":0,"name":"first","result_err":"THIS IS A PRE-EXISTING FAILURE"}`,
   129  					FailFlagKey, "this is a pre-existing failure",
   130  				),
   131  				msg(`{"id":1,"name":"second","result_err":""}`),
   132  			},
   133  		},
   134  		"filtered and failed mappings": {
   135  			requestMap: `root = match {
   136  				this.id == 0 => throw("i dont like zero"),
   137  				this.id == 3 => deleted(),
   138  				_ => {"name":this.name,"id":this.id}
   139  			}`,
   140  			processorMap: `root = this
   141  			root.name_upper = this.name.uppercase()`,
   142  			resultMap: `root.result = match {
   143  				this.id == 2 => throw("i dont like two either"),
   144  				_ => this.name_upper
   145  			}`,
   146  			input: []mockMsg{
   147  				msg(`{"id":0,"name":"first"}`),
   148  				msg(`{"id":1,"name":"second"}`),
   149  				msg(`{"id":2,"name":"third"}`),
   150  				msg(`{"id":3,"name":"fourth"}`),
   151  				msg(`{"id":4,"name":"fifth"}`),
   152  			},
   153  			output: []mockMsg{
   154  				msg(
   155  					`{"id":0,"name":"first"}`,
   156  					FailFlagKey,
   157  					"request mapping failed: failed assignment (line 1): i dont like zero",
   158  				),
   159  				msg(`{"id":1,"name":"second","result":"SECOND"}`),
   160  				msg(
   161  					`{"id":2,"name":"third"}`,
   162  					FailFlagKey,
   163  					"result mapping failed: failed assignment (line 1): i dont like two either",
   164  				),
   165  				msg(`{"id":3,"name":"fourth"}`),
   166  				msg(`{"id":4,"name":"fifth","result":"FIFTH"}`),
   167  			},
   168  		},
   169  		"filter all requests": {
   170  			requestMap:   `root = deleted()`,
   171  			processorMap: `root = this`,
   172  			resultMap:    `root.result = this`,
   173  			input: []mockMsg{
   174  				msg(`{"id":0,"name":"first"}`),
   175  				msg(`{"id":1,"name":"second"}`),
   176  				msg(`{"id":2,"name":"third"}`),
   177  				msg(`{"id":3,"name":"fourth"}`),
   178  				msg(`{"id":4,"name":"fifth"}`),
   179  			},
   180  			output: []mockMsg{
   181  				msg(`{"id":0,"name":"first"}`),
   182  				msg(`{"id":1,"name":"second"}`),
   183  				msg(`{"id":2,"name":"third"}`),
   184  				msg(`{"id":3,"name":"fourth"}`),
   185  				msg(`{"id":4,"name":"fifth"}`),
   186  			},
   187  		},
   188  		"filter during processing": {
   189  			requestMap:   `root = if this.id == 3 { throw("foo") } else { this }`,
   190  			processorMap: `root = deleted()`,
   191  			resultMap:    `root.result = this`,
   192  			input: []mockMsg{
   193  				msg(`{"id":0,"name":"first"}`),
   194  				msg(`{"id":1,"name":"second"}`),
   195  				msg(`{"id":2,"name":"third"}`),
   196  				msg(`{"id":3,"name":"fourth"}`),
   197  				msg(`{"id":4,"name":"fifth"}`),
   198  			},
   199  			output: []mockMsg{
   200  				msg(
   201  					`{"id":0,"name":"first"}`,
   202  					FailFlagKey,
   203  					"child processors resulted in zero messages",
   204  				),
   205  				msg(
   206  					`{"id":1,"name":"second"}`,
   207  					FailFlagKey,
   208  					"child processors resulted in zero messages",
   209  				),
   210  				msg(
   211  					`{"id":2,"name":"third"}`,
   212  					FailFlagKey,
   213  					"child processors resulted in zero messages",
   214  				),
   215  				msg(
   216  					`{"id":3,"name":"fourth"}`,
   217  					FailFlagKey,
   218  					"request mapping failed: failed assignment (line 1): foo",
   219  				),
   220  				msg(
   221  					`{"id":4,"name":"fifth"}`,
   222  					FailFlagKey,
   223  					"child processors resulted in zero messages",
   224  				),
   225  			},
   226  		},
   227  		"filter some during processing": {
   228  			requestMap:   `root = if this.id == 3 { throw("foo") } else { this }`,
   229  			processorMap: `root = if this.id == 2 { deleted() }`,
   230  			resultMap:    `root.result = this`,
   231  			input: []mockMsg{
   232  				msg(`{"id":0,"name":"first"}`),
   233  				msg(`{"id":1,"name":"second"}`),
   234  				msg(`{"id":2,"name":"third"}`),
   235  				msg(`{"id":3,"name":"fourth"}`),
   236  				msg(`{"id":4,"name":"fifth"}`),
   237  			},
   238  			output: []mockMsg{
   239  				msg(
   240  					`{"id":0,"name":"first"}`,
   241  					FailFlagKey,
   242  					"message count from branch processors does not match request, started with 4 messages, finished with 5",
   243  				),
   244  				msg(
   245  					`{"id":1,"name":"second"}`,
   246  					FailFlagKey,
   247  					"message count from branch processors does not match request, started with 4 messages, finished with 5",
   248  				),
   249  				msg(
   250  					`{"id":2,"name":"third"}`,
   251  					FailFlagKey,
   252  					"message count from branch processors does not match request, started with 4 messages, finished with 5",
   253  				),
   254  				msg(
   255  					`{"id":3,"name":"fourth"}`,
   256  					FailFlagKey,
   257  					"request mapping failed: failed assignment (line 1): foo",
   258  				),
   259  				msg(
   260  					`{"id":4,"name":"fifth"}`,
   261  					FailFlagKey,
   262  					"message count from branch processors does not match request, started with 4 messages, finished with 5",
   263  				),
   264  			},
   265  		},
   266  	}
   267  
   268  	for name, test := range tests {
   269  		test := test
   270  		t.Run(name, func(t *testing.T) {
   271  			t.Parallel()
   272  
   273  			procConf := NewConfig()
   274  			procConf.Type = TypeBloblang
   275  			procConf.Bloblang = BloblangConfig(test.processorMap)
   276  
   277  			conf := NewConfig()
   278  			conf.Type = TypeBranch
   279  			conf.Branch.RequestMap = test.requestMap
   280  			conf.Branch.Processors = append(conf.Branch.Processors, procConf)
   281  			conf.Branch.ResultMap = test.resultMap
   282  
   283  			proc, err := NewBranch(conf, nil, log.Noop(), metrics.Noop())
   284  			require.NoError(t, err)
   285  
   286  			msg := message.New(nil)
   287  			for _, m := range test.input {
   288  				part := message.NewPart([]byte(m.content))
   289  				if m.meta != nil {
   290  					for k, v := range m.meta {
   291  						part.Metadata().Set(k, v)
   292  					}
   293  				}
   294  				msg.Append(part)
   295  			}
   296  
   297  			outMsgs, res := proc.ProcessMessage(msg)
   298  
   299  			require.Nil(t, res)
   300  			require.Len(t, outMsgs, 1)
   301  
   302  			assert.Equal(t, len(test.output), outMsgs[0].Len())
   303  			for i, out := range test.output {
   304  				comparePart := mockMsg{
   305  					content: string(outMsgs[0].Get(i).Get()),
   306  					meta:    map[string]string{},
   307  				}
   308  
   309  				outMsgs[0].Get(i).Metadata().Iter(func(k, v string) error {
   310  					comparePart.meta[k] = v
   311  					return nil
   312  				})
   313  
   314  				assert.Equal(t, out, comparePart)
   315  			}
   316  
   317  			// Ensure nothing changed
   318  			for i, m := range test.input {
   319  				doc, err := msg.Get(i).JSON()
   320  				if err == nil {
   321  					msg.Get(i).SetJSON(doc)
   322  				}
   323  				assert.Equal(t, m.content, string(msg.Get(i).Get()))
   324  			}
   325  
   326  			proc.CloseAsync()
   327  			assert.NoError(t, proc.WaitForClose(time.Second))
   328  		})
   329  	}
   330  }