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

     1  package processor
     2  
     3  import (
     4  	"sort"
     5  	"strconv"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/Jeffail/benthos/v3/lib/log"
    11  	"github.com/Jeffail/benthos/v3/lib/message"
    12  	"github.com/Jeffail/benthos/v3/lib/metrics"
    13  	"github.com/Jeffail/benthos/v3/lib/types"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestWorkflowDeps(t *testing.T) {
    19  	tests := []struct {
    20  		branches      [][2]string
    21  		inputOrdering [][]string
    22  		ordering      [][]string
    23  		err           string
    24  	}{
    25  		{
    26  			branches: [][2]string{
    27  				{
    28  					"root = this.foo",
    29  					"root.bar = this",
    30  				},
    31  				{
    32  					"root = this.bar",
    33  					"root.baz = this",
    34  				},
    35  				{
    36  					"root = this.baz",
    37  					"root.buz = this",
    38  				},
    39  			},
    40  			ordering: [][]string{
    41  				{"0"}, {"1"}, {"2"},
    42  			},
    43  		},
    44  		{
    45  			branches: [][2]string{
    46  				{
    47  					"root = this.foo",
    48  					"root.bar = this",
    49  				},
    50  				{
    51  					"root = this.bar",
    52  					"root.baz = this",
    53  				},
    54  				{
    55  					"root = this.baz",
    56  					"root.buz = this",
    57  				},
    58  			},
    59  			inputOrdering: [][]string{
    60  				{"1", "2"}, {"0"},
    61  			},
    62  			ordering: [][]string{
    63  				{"1", "2"}, {"0"},
    64  			},
    65  		},
    66  		{
    67  			branches: [][2]string{
    68  				{
    69  					"root = this.foo",
    70  					"root.bar = this",
    71  				},
    72  				{
    73  					"root = this.bar",
    74  					"root.baz = this",
    75  				},
    76  				{
    77  					"root = this.baz",
    78  					"root.buz = this",
    79  				},
    80  			},
    81  			ordering: [][]string{
    82  				{"0"}, {"1"}, {"2"},
    83  			},
    84  		},
    85  		{
    86  			branches: [][2]string{
    87  				{
    88  					"root = this.foo",
    89  					"root.bar = this",
    90  				},
    91  				{
    92  					"root = this.foo",
    93  					"root.baz = this",
    94  				},
    95  				{
    96  					"root = this.baz",
    97  					"root.foo = this",
    98  				},
    99  			},
   100  			err: "failed to automatically resolve DAG, circular dependencies detected for branches: [0 1 2]",
   101  		},
   102  		{
   103  			branches: [][2]string{
   104  				{
   105  					"root = this.foo",
   106  					"root.bar = this",
   107  				},
   108  				{
   109  					"root = this.bar",
   110  					"root.baz = this",
   111  				},
   112  				{
   113  					"root = this.baz",
   114  					"root.buz = this",
   115  				},
   116  			},
   117  			inputOrdering: [][]string{
   118  				{"1"}, {"0"},
   119  			},
   120  			err: "the following branches were missing from order: [2]",
   121  		},
   122  		{
   123  			branches: [][2]string{
   124  				{
   125  					"root = this.foo",
   126  					"root.bar = this",
   127  				},
   128  				{
   129  					"root = this.bar",
   130  					"root.baz = this",
   131  				},
   132  				{
   133  					"root = this.baz",
   134  					"root.buz = this",
   135  				},
   136  			},
   137  			inputOrdering: [][]string{
   138  				{"1"}, {"0", "2"}, {"1"},
   139  			},
   140  			err: "branch specified in order listed multiple times: 1",
   141  		},
   142  		{
   143  			branches: [][2]string{
   144  				{
   145  					"root = this.foo",
   146  					"root.bar = this",
   147  				},
   148  				{
   149  					"root = this.foo",
   150  					"root.baz = this",
   151  				},
   152  				{
   153  					`root.bar = this.bar
   154  					root.baz = this.baz`,
   155  					"root.buz = this",
   156  				},
   157  			},
   158  			ordering: [][]string{
   159  				{"0", "1"}, {"2"},
   160  			},
   161  		},
   162  	}
   163  
   164  	for i, test := range tests {
   165  		test := test
   166  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   167  			conf := NewConfig()
   168  			conf.Workflow.Order = test.inputOrdering
   169  			for j, mappings := range test.branches {
   170  				branchConf := NewBranchConfig()
   171  				branchConf.RequestMap = mappings[0]
   172  				branchConf.ResultMap = mappings[1]
   173  				dudProc := NewConfig()
   174  				dudProc.Type = TypeBloblang
   175  				dudProc.Bloblang = BloblangConfig("root = this")
   176  				branchConf.Processors = append(branchConf.Processors, dudProc)
   177  				conf.Workflow.Branches[strconv.Itoa(j)] = branchConf
   178  			}
   179  
   180  			p, err := NewWorkflow(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   181  			if len(test.err) > 0 {
   182  				assert.EqualError(t, err, test.err)
   183  			} else {
   184  				require.NoError(t, err)
   185  
   186  				dag := p.(*Workflow).children.dag
   187  				for _, d := range dag {
   188  					sort.Strings(d)
   189  				}
   190  				assert.Equal(t, test.ordering, dag)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func newMockProcProvider(t *testing.T, confs map[string]Config) types.Manager {
   197  	t.Helper()
   198  
   199  	procs := map[string]Type{}
   200  
   201  	for k, v := range confs {
   202  		var err error
   203  		procs[k], err = New(v, nil, log.Noop(), metrics.Noop())
   204  		require.NoError(t, err)
   205  	}
   206  
   207  	return &fakeProcMgr{
   208  		procs: procs,
   209  	}
   210  }
   211  
   212  func quickTestBranches(branches ...[4]string) map[string]Config {
   213  	m := map[string]Config{}
   214  	for _, b := range branches {
   215  		blobConf := NewConfig()
   216  		blobConf.Type = TypeBloblang
   217  		blobConf.Bloblang = BloblangConfig(b[2])
   218  
   219  		conf := NewConfig()
   220  		conf.Type = TypeBranch
   221  		conf.Branch.RequestMap = b[1]
   222  		conf.Branch.Processors = append(conf.Branch.Processors, blobConf)
   223  		conf.Branch.ResultMap = b[3]
   224  
   225  		m[b[0]] = conf
   226  	}
   227  	return m
   228  }
   229  
   230  func TestWorkflowMissingResources(t *testing.T) {
   231  	conf := NewConfig()
   232  	conf.Workflow.Order = [][]string{
   233  		{"foo", "bar", "baz"},
   234  	}
   235  
   236  	branchConf := NewConfig()
   237  	branchConf.Branch.RequestMap = "root = this"
   238  	branchConf.Branch.ResultMap = "root = this"
   239  
   240  	blobConf := NewConfig()
   241  	blobConf.Type = TypeBloblang
   242  	blobConf.Bloblang = "root = this"
   243  
   244  	branchConf.Branch.Processors = append(branchConf.Branch.Processors, blobConf)
   245  
   246  	conf.Workflow.Branches["bar"] = branchConf.Branch
   247  
   248  	mgr := newMockProcProvider(t, map[string]Config{
   249  		"baz": branchConf,
   250  	})
   251  
   252  	_, err := NewWorkflow(conf, mgr, log.Noop(), metrics.Noop())
   253  	require.EqualError(t, err, "processor resource 'foo' was not found")
   254  }
   255  
   256  func TestWorkflows(t *testing.T) {
   257  	type mockMsg struct {
   258  		content string
   259  		meta    map[string]string
   260  	}
   261  	msg := func(content string, meta ...string) mockMsg {
   262  		t.Helper()
   263  		m := mockMsg{
   264  			content: content,
   265  			meta:    map[string]string{},
   266  		}
   267  		for i, v := range meta {
   268  			if i%2 == 1 {
   269  				m.meta[meta[i-1]] = v
   270  			}
   271  		}
   272  		return m
   273  	}
   274  
   275  	// To make configs simpler they break branches down into three mappings, the
   276  	// request map, a bloblang processor, and a result map.
   277  	tests := []struct {
   278  		branches [][3]string
   279  		order    [][]string
   280  		input    []mockMsg
   281  		output   []mockMsg
   282  		err      string
   283  	}{
   284  		{
   285  			branches: [][3]string{
   286  				{
   287  					"root.foo = this.foo.not_null()",
   288  					"root = this",
   289  					"root.bar = this.foo.number()",
   290  				},
   291  			},
   292  			input: []mockMsg{
   293  				msg(`{}`),
   294  				msg(`{"foo":"not a number"}`),
   295  				msg(`{"foo":"5"}`),
   296  			},
   297  			output: []mockMsg{
   298  				msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`),
   299  				msg(`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`),
   300  				msg(`{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`),
   301  			},
   302  		},
   303  		{
   304  			branches: [][3]string{
   305  				{
   306  					"root.foo = this.foo.not_null()",
   307  					"root = this",
   308  					"root.bar = this.foo.number()",
   309  				},
   310  				{
   311  					"root.bar = this.bar.not_null()",
   312  					"root = this",
   313  					"root.baz = this.bar.number() + 5",
   314  				},
   315  				{
   316  					"root.baz = this.baz.not_null()",
   317  					"root = this",
   318  					"root.buz = this.baz.number() + 2",
   319  				},
   320  			},
   321  			input: []mockMsg{
   322  				msg(`{}`),
   323  				msg(`{"foo":"not a number"}`),
   324  				msg(`{"foo":"5"}`),
   325  			},
   326  			output: []mockMsg{
   327  				msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`),
   328  				msg(`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`),
   329  				msg(`{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`),
   330  			},
   331  		},
   332  		{
   333  			branches: [][3]string{
   334  				{
   335  					"root.foo = this.foo.not_null()",
   336  					"root = this",
   337  					"root.bar = this.foo.number()",
   338  				},
   339  				{
   340  					"root.bar = this.bar.not_null()",
   341  					"root = this",
   342  					"root.baz = this.bar.number() + 5",
   343  				},
   344  				{
   345  					"root.baz = this.baz.not_null()",
   346  					"root = this",
   347  					"root.buz = this.baz.number() + 2",
   348  				},
   349  			},
   350  			input: []mockMsg{
   351  				msg(`{"meta":{"workflow":{"apply":["2"]}},"baz":2}`),
   352  				msg(`{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`),
   353  				msg(`{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`),
   354  			},
   355  			output: []mockMsg{
   356  				msg(`{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`),
   357  				msg(`{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`),
   358  				msg(`{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`),
   359  			},
   360  		},
   361  		{
   362  			branches: [][3]string{
   363  				{
   364  					"root = this.foo.not_null()",
   365  					"root = this",
   366  					"root.bar = this.number() + 2",
   367  				},
   368  				{
   369  					"root = this.foo.not_null()",
   370  					"root = this",
   371  					"root.baz = this.number() + 3",
   372  				},
   373  				{
   374  					`root.bar = this.bar.not_null()
   375  					root.baz = this.baz.not_null()`,
   376  					"root = this",
   377  					"root.buz = this.bar + this.baz",
   378  				},
   379  			},
   380  			input: []mockMsg{
   381  				msg(`{"foo":2}`),
   382  				msg(`{}`),
   383  				msg(`not even a json object`),
   384  			},
   385  			output: []mockMsg{
   386  				msg(`{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`),
   387  				msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`),
   388  				msg(
   389  					`not even a json object`,
   390  					FailFlagKey,
   391  					"invalid character 'o' in literal null (expecting 'u')",
   392  				),
   393  			},
   394  		},
   395  		{
   396  			branches: [][3]string{
   397  				{
   398  					`root = this`,
   399  					`root = this
   400  					 root.name_upper = this.name.uppercase()`,
   401  					`root.result = if this.failme.bool(false) {
   402  						throw("this is a branch error")
   403  					} else {
   404  						this.name_upper
   405  					}`,
   406  				},
   407  			},
   408  			input: []mockMsg{
   409  				msg(
   410  					`{"id":0,"name":"first"}`,
   411  					FailFlagKey, "this is a pre-existing failure",
   412  				),
   413  				msg(`{"failme":true,"id":1,"name":"second"}`),
   414  				msg(
   415  					`{"failme":true,"id":2,"name":"third"}`,
   416  					FailFlagKey, "this is a pre-existing failure",
   417  				),
   418  			},
   419  			output: []mockMsg{
   420  				msg(
   421  					`{"id":0,"meta":{"workflow":{"succeeded":["0"]}},"name":"first","result":"FIRST"}`,
   422  					FailFlagKey, "this is a pre-existing failure",
   423  				),
   424  				msg(
   425  					`{"failme":true,"id":1,"meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): this is a branch error"}}},"name":"second"}`,
   426  				),
   427  				msg(
   428  					`{"failme":true,"id":2,"meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): this is a branch error"}}},"name":"third"}`,
   429  					FailFlagKey, "this is a pre-existing failure",
   430  				),
   431  			},
   432  		},
   433  	}
   434  
   435  	for i, test := range tests {
   436  		test := test
   437  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   438  			conf := NewConfig()
   439  			conf.Workflow.Order = test.order
   440  			for j, mappings := range test.branches {
   441  				branchConf := NewBranchConfig()
   442  				branchConf.RequestMap = mappings[0]
   443  				branchConf.ResultMap = mappings[2]
   444  				proc := NewConfig()
   445  				proc.Type = TypeBloblang
   446  				proc.Bloblang = BloblangConfig(mappings[1])
   447  				branchConf.Processors = append(branchConf.Processors, proc)
   448  				conf.Workflow.Branches[strconv.Itoa(j)] = branchConf
   449  			}
   450  
   451  			p, err := NewWorkflow(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   452  			require.NoError(t, err)
   453  
   454  			inputMsg := message.New(nil)
   455  			for _, m := range test.input {
   456  				part := message.NewPart([]byte(m.content))
   457  				if m.meta != nil {
   458  					for k, v := range m.meta {
   459  						part.Metadata().Set(k, v)
   460  					}
   461  				}
   462  				inputMsg.Append(part)
   463  			}
   464  
   465  			msgs, res := p.ProcessMessage(inputMsg)
   466  			if len(test.err) > 0 {
   467  				require.NotNil(t, res)
   468  				require.EqualError(t, res.Error(), test.err)
   469  			} else {
   470  				require.Len(t, msgs, 1)
   471  				assert.Equal(t, len(test.output), msgs[0].Len())
   472  				for i, out := range test.output {
   473  					comparePart := mockMsg{
   474  						content: string(msgs[0].Get(i).Get()),
   475  						meta:    map[string]string{},
   476  					}
   477  
   478  					msgs[0].Get(i).Metadata().Iter(func(k, v string) error {
   479  						comparePart.meta[k] = v
   480  						return nil
   481  					})
   482  
   483  					assert.Equal(t, out, comparePart, "part: %v", i)
   484  				}
   485  			}
   486  
   487  			// Ensure nothing changed
   488  			for i, m := range test.input {
   489  				assert.Equal(t, m.content, string(inputMsg.Get(i).Get()))
   490  			}
   491  
   492  			p.CloseAsync()
   493  			assert.NoError(t, p.WaitForClose(time.Second))
   494  		})
   495  	}
   496  }
   497  
   498  func TestWorkflowsWithResources(t *testing.T) {
   499  	// To make configs simpler they break branches down into three mappings, the
   500  	// request map, a bloblang processor, and a result map.
   501  	tests := []struct {
   502  		branches [][4]string
   503  		input    []string
   504  		output   []string
   505  		err      string
   506  	}{
   507  		{
   508  			branches: [][4]string{
   509  				{
   510  					"0",
   511  					"root.foo = this.foo.not_null()",
   512  					"root = this",
   513  					"root.bar = this.foo.number()",
   514  				},
   515  			},
   516  			input: []string{
   517  				`{}`,
   518  				`{"foo":"not a number"}`,
   519  				`{"foo":"5"}`,
   520  			},
   521  			output: []string{
   522  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`,
   523  				`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`,
   524  				`{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`,
   525  			},
   526  		},
   527  		{
   528  			branches: [][4]string{
   529  				{
   530  					"0",
   531  					"root.foo = this.foo.not_null()",
   532  					"root = this",
   533  					"root.bar = this.foo.number()",
   534  				},
   535  				{
   536  					"1",
   537  					"root.bar = this.bar.not_null()",
   538  					"root = this",
   539  					"root.baz = this.bar.number() + 5",
   540  				},
   541  				{
   542  					"2",
   543  					"root.baz = this.baz.not_null()",
   544  					"root = this",
   545  					"root.buz = this.baz.number() + 2",
   546  				},
   547  			},
   548  			input: []string{
   549  				`{}`,
   550  				`{"foo":"not a number"}`,
   551  				`{"foo":"5"}`,
   552  			},
   553  			output: []string{
   554  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   555  				`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   556  				`{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`,
   557  			},
   558  		},
   559  		{
   560  			branches: [][4]string{
   561  				{
   562  					"0",
   563  					"root.foo = this.foo.not_null()",
   564  					"root = this",
   565  					"root.bar = this.foo.number()",
   566  				},
   567  				{
   568  					"1",
   569  					"root.bar = this.bar.not_null()",
   570  					"root = this",
   571  					"root.baz = this.bar.number() + 5",
   572  				},
   573  				{
   574  					"2",
   575  					"root.baz = this.baz.not_null()",
   576  					"root = this",
   577  					"root.buz = this.baz.number() + 2",
   578  				},
   579  			},
   580  			input: []string{
   581  				`{"meta":{"workflow":{"apply":["2"]}},"baz":2}`,
   582  				`{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`,
   583  				`{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`,
   584  			},
   585  			output: []string{
   586  				`{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`,
   587  				`{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`,
   588  				`{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`,
   589  			},
   590  		},
   591  		{
   592  			branches: [][4]string{
   593  				{
   594  					"0",
   595  					"root = this.foo.not_null()",
   596  					"root = this",
   597  					"root.bar = this.number() + 2",
   598  				},
   599  				{
   600  					"1",
   601  					"root = this.foo.not_null()",
   602  					"root = this",
   603  					"root.baz = this.number() + 3",
   604  				},
   605  				{
   606  					"2",
   607  					`root.bar = this.bar.not_null()
   608  					root.baz = this.baz.not_null()`,
   609  					"root = this",
   610  					"root.buz = this.bar + this.baz",
   611  				},
   612  			},
   613  			input: []string{
   614  				`{"foo":2}`,
   615  				`{}`,
   616  				`not even a json object`,
   617  			},
   618  			output: []string{
   619  				`{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`,
   620  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`,
   621  				`not even a json object`,
   622  			},
   623  		},
   624  	}
   625  
   626  	for i, test := range tests {
   627  		test := test
   628  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   629  			conf := NewConfig()
   630  			conf.Workflow.BranchResources = []string{}
   631  			for _, b := range test.branches {
   632  				conf.Workflow.BranchResources = append(conf.Workflow.BranchResources, b[0])
   633  			}
   634  
   635  			mgr := newMockProcProvider(t, quickTestBranches(test.branches...))
   636  			p, err := NewWorkflow(conf, mgr, log.Noop(), metrics.Noop())
   637  			require.NoError(t, err)
   638  
   639  			var parts [][]byte
   640  			for _, input := range test.input {
   641  				parts = append(parts, []byte(input))
   642  			}
   643  
   644  			msgs, res := p.ProcessMessage(message.New(parts))
   645  			if len(test.err) > 0 {
   646  				require.NotNil(t, res)
   647  				require.EqualError(t, res.Error(), test.err)
   648  			} else {
   649  				require.Len(t, msgs, 1)
   650  				var output []string
   651  				for _, b := range message.GetAllBytes(msgs[0]) {
   652  					output = append(output, string(b))
   653  				}
   654  				assert.Equal(t, test.output, output)
   655  			}
   656  
   657  			p.CloseAsync()
   658  			assert.NoError(t, p.WaitForClose(time.Second))
   659  		})
   660  	}
   661  }
   662  
   663  func TestWorkflowsParallel(t *testing.T) {
   664  	branches := [][4]string{
   665  		{
   666  			"0",
   667  			"root.foo = this.foo.not_null()",
   668  			"root = this",
   669  			"root.bar = this.foo.number()",
   670  		},
   671  		{
   672  			"1",
   673  			"root.bar = this.bar.not_null()",
   674  			"root = this",
   675  			"root.baz = this.bar.number() + 5",
   676  		},
   677  		{
   678  			"2",
   679  			"root.baz = this.baz.not_null()",
   680  			"root = this",
   681  			"root.buz = this.baz.number() + 2",
   682  		},
   683  	}
   684  	input := []string{
   685  		`{}`,
   686  		`{"foo":"not a number"}`,
   687  		`{"foo":"5"}`,
   688  	}
   689  	output := []string{
   690  		`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   691  		`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   692  		`{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`,
   693  	}
   694  
   695  	conf := NewConfig()
   696  	conf.Workflow.BranchResources = []string{}
   697  	for _, b := range branches {
   698  		conf.Workflow.BranchResources = append(conf.Workflow.BranchResources, b[0])
   699  	}
   700  
   701  	for loops := 0; loops < 10; loops++ {
   702  		mgr := newMockProcProvider(t, quickTestBranches(branches...))
   703  		p, err := NewWorkflow(conf, mgr, log.Noop(), metrics.Noop())
   704  		require.NoError(t, err)
   705  
   706  		startChan := make(chan struct{})
   707  		wg := sync.WaitGroup{}
   708  
   709  		for i := 0; i < 10; i++ {
   710  			wg.Add(1)
   711  			go func() {
   712  				defer wg.Done()
   713  				<-startChan
   714  
   715  				for j := 0; j < 100; j++ {
   716  					var parts [][]byte
   717  					for _, input := range input {
   718  						parts = append(parts, []byte(input))
   719  					}
   720  
   721  					msgs, res := p.ProcessMessage(message.New(parts))
   722  					require.Nil(t, res)
   723  					require.Len(t, msgs, 1)
   724  					var actual []string
   725  					for _, b := range message.GetAllBytes(msgs[0]) {
   726  						actual = append(actual, string(b))
   727  					}
   728  					assert.Equal(t, output, actual)
   729  				}
   730  			}()
   731  		}
   732  
   733  		close(startChan)
   734  		wg.Wait()
   735  
   736  		p.CloseAsync()
   737  		assert.NoError(t, p.WaitForClose(time.Second))
   738  	}
   739  }
   740  
   741  func TestWorkflowsWithOrderResources(t *testing.T) {
   742  	// To make configs simpler they break branches down into three mappings, the
   743  	// request map, a bloblang processor, and a result map.
   744  	tests := []struct {
   745  		branches [][4]string
   746  		order    [][]string
   747  		input    []string
   748  		output   []string
   749  		err      string
   750  	}{
   751  		{
   752  			branches: [][4]string{
   753  				{
   754  					"0",
   755  					"root.foo = this.foo.not_null()",
   756  					"root = this",
   757  					"root.bar = this.foo.number()",
   758  				},
   759  			},
   760  			order: [][]string{
   761  				{"0"},
   762  			},
   763  			input: []string{
   764  				`{}`,
   765  				`{"foo":"not a number"}`,
   766  				`{"foo":"5"}`,
   767  			},
   768  			output: []string{
   769  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`,
   770  				`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`,
   771  				`{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`,
   772  			},
   773  		},
   774  		{
   775  			branches: [][4]string{
   776  				{
   777  					"0",
   778  					"root.foo = this.foo.not_null()",
   779  					"root = this",
   780  					"root.bar = this.foo.number()",
   781  				},
   782  				{
   783  					"1",
   784  					"root.bar = this.bar.not_null()",
   785  					"root = this",
   786  					"root.baz = this.bar.number() + 5",
   787  				},
   788  				{
   789  					"2",
   790  					"root.baz = this.baz.not_null()",
   791  					"root = this",
   792  					"root.buz = this.baz.number() + 2",
   793  				},
   794  			},
   795  			order: [][]string{
   796  				{"0"},
   797  				{"1"},
   798  				{"2"},
   799  			},
   800  			input: []string{
   801  				`{}`,
   802  				`{"foo":"not a number"}`,
   803  				`{"foo":"5"}`,
   804  			},
   805  			output: []string{
   806  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   807  				`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`,
   808  				`{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`,
   809  			},
   810  		},
   811  		{
   812  			branches: [][4]string{
   813  				{
   814  					"0",
   815  					"root.foo = this.foo.not_null()",
   816  					"root = this",
   817  					"root.bar = this.foo.number()",
   818  				},
   819  				{
   820  					"1",
   821  					"root.bar = this.bar.not_null()",
   822  					"root = this",
   823  					"root.baz = this.bar.number() + 5",
   824  				},
   825  				{
   826  					"2",
   827  					"root.baz = this.baz.not_null()",
   828  					"root = this",
   829  					"root.buz = this.baz.number() + 2",
   830  				},
   831  			},
   832  			order: [][]string{
   833  				{"0"},
   834  				{"1"},
   835  				{"2"},
   836  			},
   837  			input: []string{
   838  				`{"meta":{"workflow":{"apply":["2"]}},"baz":2}`,
   839  				`{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`,
   840  				`{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`,
   841  			},
   842  			output: []string{
   843  				`{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`,
   844  				`{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`,
   845  				`{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`,
   846  			},
   847  		},
   848  		{
   849  			branches: [][4]string{
   850  				{
   851  					"0",
   852  					"root = this.foo.not_null()",
   853  					"root = this",
   854  					"root.bar = this.number() + 2",
   855  				},
   856  				{
   857  					"1",
   858  					"root = this.foo.not_null()",
   859  					"root = this",
   860  					"root.baz = this.number() + 3",
   861  				},
   862  				{
   863  					"2",
   864  					`root.bar = this.bar.not_null()
   865  					root.baz = this.baz.not_null()`,
   866  					"root = this",
   867  					"root.buz = this.bar + this.baz",
   868  				},
   869  			},
   870  			order: [][]string{
   871  				{"0", "1"},
   872  				{"2"},
   873  			},
   874  			input: []string{
   875  				`{"foo":2}`,
   876  				`{}`,
   877  				`not even a json object`,
   878  			},
   879  			output: []string{
   880  				`{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`,
   881  				`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`,
   882  				`not even a json object`,
   883  			},
   884  		},
   885  	}
   886  
   887  	for i, test := range tests {
   888  		test := test
   889  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   890  			conf := NewConfig()
   891  			conf.Workflow.Order = test.order
   892  
   893  			mgr := newMockProcProvider(t, quickTestBranches(test.branches...))
   894  			p, err := NewWorkflow(conf, mgr, log.Noop(), metrics.Noop())
   895  			require.NoError(t, err)
   896  
   897  			var parts [][]byte
   898  			for _, input := range test.input {
   899  				parts = append(parts, []byte(input))
   900  			}
   901  
   902  			msgs, res := p.ProcessMessage(message.New(parts))
   903  			if len(test.err) > 0 {
   904  				require.NotNil(t, res)
   905  				require.EqualError(t, res.Error(), test.err)
   906  			} else {
   907  				require.Len(t, msgs, 1)
   908  				var output []string
   909  				for _, b := range message.GetAllBytes(msgs[0]) {
   910  					output = append(output, string(b))
   911  				}
   912  				assert.Equal(t, test.output, output)
   913  			}
   914  
   915  			p.CloseAsync()
   916  			assert.NoError(t, p.WaitForClose(time.Second))
   917  		})
   918  	}
   919  }