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 }