github.com/Comcast/plax@v0.8.32/dsl/spec.go (about) 1 /* 2 * Copyright 2021 Comcast Cable Communications Management, LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * SPDX-License-Identifier: Apache-2.0 17 */ 18 19 package dsl 20 21 import ( 22 "encoding/json" 23 "fmt" 24 "strings" 25 "time" 26 27 "github.com/Comcast/plax/subst" 28 "github.com/Comcast/sheens/match" 29 jschema "github.com/xeipuuv/gojsonschema" 30 ) 31 32 var DefaultInitialPhase = "phase1" 33 34 // Spec represents a set of named test Phases. 35 type Spec struct { 36 // InitialPhase is the starting phase, which defaults to 37 // DefaultInitialPhase. 38 InitialPhase string 39 40 // FinalPhases is an option list of phases to execute after 41 // the execution starting at InitialPhase terminates. 42 FinalPhases []string 43 44 // Phases maps phase names to Phases. 45 // 46 // Each Phase is subject to bindings substitution. 47 Phases map[string]*Phase 48 } 49 50 func NewSpec() *Spec { 51 return &Spec{ 52 InitialPhase: DefaultInitialPhase, 53 Phases: make(map[string]*Phase), 54 } 55 } 56 57 // Phase is a list of Steps. 58 type Phase struct { 59 // Doc is an optional documentation string. 60 Doc string `yaml:",omitempty"` 61 62 // Steps is a sequence of Steps, which are attempted in order. 63 // 64 // Each Step is subject to bindings substitution. 65 Steps []*Step 66 } 67 68 func (p *Phase) AddStep(ctx *Ctx, s *Step) { 69 steps := p.Steps 70 if steps == nil { 71 steps = make([]*Step, 0, 8) 72 } 73 p.Steps = append(steps, s) 74 } 75 76 func (p *Phase) Exec(ctx *Ctx, t *Test) (string, error) { 77 var ( 78 next string 79 err error 80 last = len(p.Steps) - 1 81 ) 82 for i, s := range p.Steps { 83 ctx.Indf(" Step %d", i) 84 ctx.Inddf(" Bindings: %s", JSON(t.Bindings)) 85 86 if next, err = s.exec(ctx, t); err != nil { 87 _, broke := IsBroken(err) 88 err := fmt.Errorf("step %d: %w", i, err) 89 if broke { 90 return "", NewBroken(err) 91 } else { 92 return "", err 93 } 94 } 95 if i < last && next != "" { 96 return "", Brokenf("Goto or Branch not last in %s", JSON(p)) 97 } 98 if i == last { 99 ctx.Indf(" Next phase: '%s'", next) 100 } 101 } 102 return next, err 103 } 104 105 // Step represents a single action. 106 type Step struct { 107 // Doc is an optional documentation string. 108 Doc string `yaml:",omitempty"` 109 110 // Fails indicates that this Step is expected to fail, which 111 // currently means returning an error from exec. 112 Fails bool `yaml:",omitempty"` 113 114 // Skip will make the test execution skip this step. 115 Skip bool `yaml:",omitempty"` 116 117 Pub *Pub `yaml:",omitempty"` 118 Sub *Sub `yaml:",omitempty"` 119 Recv *Recv `yaml:",omitempty"` 120 Kill *Kill `yaml:",omitempty"` 121 Reconnect *Reconnect `yaml:",omitempty"` 122 Close *Close `yaml:",omitempty"` 123 Run string `yaml:",omitempty"` 124 125 // Wait is wait time in milliseconds as a string. 126 Wait string `yaml:",omitempty"` 127 128 Goto string `yaml:",omitempty"` 129 130 Branch string `yaml:",omitempty"` 131 132 Ingest *Ingest `yaml:",omitempty"` 133 } 134 135 // exec calls exe() and then handles Fails (if any). 136 func (s *Step) exec(ctx *Ctx, t *Test) (string, error) { 137 next, err := s.exe(ctx, t) 138 if err != nil { 139 if _, is := IsBroken(err); is { 140 return "", err 141 } 142 if s.Fails { 143 return s.Goto, nil 144 } 145 return "", err 146 } 147 148 return next, err 149 } 150 151 // exe executes the step. 152 // 153 // Called by exec(). 154 func (s *Step) exe(ctx *Ctx, t *Test) (string, error) { 155 // ToDo: Warn if multiple Pub, Sub, Recv, Wait, Goto specified? 156 157 t.Tick(ctx) 158 159 if s.Skip { 160 ctx.Indf(" Skip") 161 return "", nil 162 } 163 164 if s.Pub != nil { 165 ctx.Indf(" Pub to %s", s.Pub.Chan) 166 167 e, err := s.Pub.Substitute(ctx, t) 168 if err != nil { 169 return "", err 170 } 171 172 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 173 return "", err 174 } 175 176 if err := e.Exec(ctx, t); err != nil { 177 return "", err 178 } 179 } 180 if s.Sub != nil { 181 ctx.Indf(" Sub %s", s.Sub.Chan) 182 183 e, err := s.Sub.Substitute(ctx, t) 184 if err != nil { 185 return "", err 186 } 187 188 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 189 return "", err 190 } 191 192 if err := e.Exec(ctx, t); err != nil { 193 return "", err 194 } 195 } 196 if s.Recv != nil { 197 ctx.Indf(" Recv %s", s.Recv.Chan) 198 199 e, err := s.Recv.Substitute(ctx, t) 200 if err != nil { 201 return "", err 202 } 203 204 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 205 return "", err 206 } 207 208 if err := e.Exec(ctx, t); err != nil { 209 return "", err 210 } 211 } 212 if s.Reconnect != nil { 213 ctx.Indf(" Reconnect %s", s.Reconnect.Chan) 214 215 e, err := s.Reconnect.Substitute(ctx, t) 216 if err != nil { 217 return "", err 218 } 219 220 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 221 return "", err 222 } 223 224 if err := e.Exec(ctx, t); err != nil { 225 return "", err 226 } 227 } 228 if s.Close != nil { 229 ctx.Indf(" Close %s", s.Close.Chan) 230 231 e, err := s.Close.Substitute(ctx, t) 232 if err != nil { 233 return "", err 234 } 235 236 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 237 return "", err 238 } 239 240 if err := e.Exec(ctx, t); err != nil { 241 return "", err 242 } 243 } 244 if s.Ingest != nil { 245 ctx.Indf(" Ingest %s", s.Ingest.Chan) 246 247 e, err := s.Ingest.Substitute(ctx, t) 248 if err != nil { 249 return "", err 250 } 251 252 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 253 return "", err 254 } 255 256 if err := e.Exec(ctx, t); err != nil { 257 return "", err 258 } 259 } 260 261 if s.Kill != nil { 262 ctx.Indf(" Kill %s", s.Kill.Chan) 263 264 e, err := s.Kill.Substitute(ctx, t) 265 if err != nil { 266 return "", err 267 } 268 269 if err := t.ensureChan(ctx, e.Chan, &e.ch); err != nil { 270 return "", err 271 } 272 273 if err := e.Exec(ctx, t); err != nil { 274 return "", err 275 } 276 } 277 278 if s.Branch != "" { 279 ctx.Indf(" Branch %s", short(s.Branch)) 280 281 src, err := t.Bindings.StringSub(ctx, s.Branch) 282 if err != nil { 283 return "", err 284 } 285 286 if src, err = t.prepareSource(ctx, src); err != nil { 287 return "", err 288 } 289 290 x, err := JSExec(ctx, src, t.jsEnv(ctx)) 291 if err != nil { 292 return "", err 293 } 294 295 target, is := x.(string) 296 if !is { 297 return "", Brokenf("Branch Javascript returned a %T (%#v) and not a %T", x, x, target) 298 } 299 300 ctx.Indf(" Branch returned '%s'", target) 301 302 return target, nil 303 } 304 305 if s.Run != "" { 306 ctx.Indf(" Run %s", short(s.Run)) 307 308 src, err := t.Bindings.StringSub(ctx, s.Run) 309 if err != nil { 310 return "", err 311 } 312 313 if src, err = t.prepareSource(ctx, src); err != nil { 314 return "", err 315 } 316 317 _, err = JSExec(ctx, src, t.jsEnv(ctx)) 318 319 ctx.Inddf(" Bindings: %s", JSON(t.Bindings)) 320 321 return "", err 322 } 323 324 if s.Wait != "" { 325 ctx.Indf(" Wait %s", s.Wait) 326 327 duration, err := t.Bindings.StringSub(ctx, s.Wait) 328 if err != nil { 329 return "", err 330 } 331 332 if err := Wait(ctx, duration); err != nil { 333 return "", err 334 } 335 336 return "", nil 337 } 338 339 return s.Goto, nil 340 } 341 342 // Wait will attempt to parse the duration and then sleep accordingly. 343 func Wait(ctx *Ctx, durationString string) error { 344 d, err := time.ParseDuration(durationString) 345 if err != nil { 346 return Brokenf("error parsing Wait '%s'", durationString) 347 } 348 349 time.Sleep(d) 350 351 return nil 352 } 353 354 type Pub struct { 355 Chan string 356 Topic string 357 358 // Schema is an optional URI for a JSON Schema that's used to 359 // validate outgoing messages. 360 Schema string `json:",omitempty" yaml:",omitempty"` 361 362 Payload interface{} 363 364 payload string 365 366 // Serialization specifies how a string Payload should be 367 // deserialized (if at all). 368 // 369 // Legal values: 'json', 'text'. Default is 'json'. 370 // 371 // If given a non-string, that value is always used as is. 372 // 373 // If given a string, if serialization is 'json' or not 374 // specified, then the string is parsed as JSON. If the 375 // serialization is 'text', then the string is used as is. 376 Serialization string `json:",omitempty" yaml:",omitempty"` 377 378 Run string `json:",omitempty" yaml:",omitempty"` 379 380 ch Chan 381 } 382 383 func (p *Pub) Substitute(ctx *Ctx, t *Test) (*Pub, error) { 384 385 topic, err := t.Bindings.StringSub(ctx, p.Topic) 386 if err != nil { 387 return nil, err 388 } 389 ctx.Inddf(" Effective topic: %s", topic) 390 391 payload, err := t.Bindings.SerialSub(ctx, p.Serialization, p.Payload) 392 if err != nil { 393 return nil, err 394 } 395 396 ctx.Inddf(" Effective payload: %s", payload) 397 398 run, err := t.Bindings.StringSub(ctx, p.Run) 399 if err != nil { 400 return nil, err 401 } 402 if run != "" { 403 ctx.Inddf(" Effective code (run): %s", run) 404 } 405 406 return &Pub{ 407 Chan: p.Chan, 408 Topic: topic, 409 Payload: p.Payload, 410 Serialization: p.Serialization, 411 payload: payload, 412 Run: run, 413 ch: p.ch, 414 }, nil 415 416 } 417 418 func (p *Pub) Exec(ctx *Ctx, t *Test) error { 419 ctx.Indf(" Pub topic '%s'", p.Topic) 420 ctx.Inddf(" payload %s", p.payload) 421 422 if p.Schema != "" { 423 if err := validateSchema(ctx, p.Schema, p.payload); err != nil { 424 return err 425 } 426 } 427 428 err := p.ch.Pub(ctx, Msg{ 429 Topic: p.Topic, 430 Payload: p.payload, 431 }) 432 433 if err != nil { 434 return err 435 } 436 437 if p.Run != "" { 438 src, err := t.prepareSource(ctx, p.Run) 439 if err != nil { 440 return err 441 } 442 443 env := map[string]interface{}{ 444 "test": t, 445 "elapsed": float64(t.elapsed) / 1000 / 1000, // Milliseconds 446 } 447 if _, err = JSExec(ctx, src, env); err != nil { 448 return err 449 } 450 } 451 452 return nil 453 454 } 455 456 type Sub struct { 457 Chan string 458 Topic string 459 460 // Pattern, which is deprecated, is really 'Topic'. 461 Pattern string 462 463 ch Chan 464 } 465 466 func (s *Sub) Substitute(ctx *Ctx, t *Test) (*Sub, error) { 467 468 // Backwards compatibility. 469 if s.Pattern != "" { 470 ctx.Indf("warning: Sub.Pattern is deprecated. Use Sub.Topic instead.") 471 if s.Topic != "" { 472 return nil, fmt.Errorf("just specify Topic (and not Pattern, which is deprecated)") 473 } 474 s.Topic = s.Pattern // We'll use s.Topic from here on. 475 s.Pattern = "" 476 } 477 pat, err := t.Bindings.StringSub(ctx, s.Topic) 478 if err != nil { 479 return nil, err 480 } 481 return &Sub{ 482 Chan: s.Chan, 483 Topic: pat, 484 ch: s.ch, 485 }, nil 486 } 487 488 func (s *Sub) Exec(ctx *Ctx, t *Test) error { 489 ctx.Indf(" Sub %s", s.Topic) 490 return s.ch.Sub(ctx, s.Topic) 491 } 492 493 type Recv struct { 494 Chan string 495 Topic string 496 497 // Pattern is a Sheens pattern 498 // https://github.com/Comcast/sheens/blob/main/README.md#pattern-matching 499 // for matching incoming messages. 500 // 501 // Use a pattern for matching JSON-serialized messages. 502 // 503 // Also see Regexp. 504 Pattern interface{} 505 506 // Regexp, which is an alternative to Pattern, gives a (Go) 507 // regular expression used to match incoming messages. 508 // 509 // A named group match becomes a bound variable. 510 Regexp string 511 512 Timeout time.Duration 513 514 // Target is an optional switch to specify what part of the 515 // incoming message is considered for matching. 516 // 517 // By default, only the payload is matched. If Target is 518 // "message", then matching is performed against 519 // 520 // {"Topic":TOPIC,"Payload":PAYLOAD} 521 // 522 // which allows matching based on the topic of in-bound 523 // messages. 524 Target string 525 526 // ClearBindings will remove all bindings for variables that 527 // do not start with '?!' before executing this step. 528 ClearBindings bool 529 530 // Guard is optional Javascript (!) that should return a 531 // boolean to indicate whether this Recv has been satisfied. 532 // 533 // The code is executed in a function body, and the code 534 // should 'return' a boolean. 535 // 536 // The following variables are bound in the global 537 // environment: 538 // 539 // bindingss: the set (array) of bindings returned by match() 540 // 541 // elapsed: the elapsed time in milliseconds since the last step 542 // 543 // msg: the receved message ({"topic":TOPIC,"payload":PAYLOAD}) 544 // 545 // print: a function that prints its arguments to stdout. 546 // 547 Guard string `json:",omitempty" yaml:",omitempty"` 548 549 Run string `json:",omitempty" yaml:",omitempty"` 550 551 // Schema is an optional URI for a JSON Schema that's used to 552 // validate incoming messages before other processing. 553 Schema string `json:",omitempty" yaml:",omitempty"` 554 555 // Max attempts to receive a message; optionally for a specific topic 556 Attempts int `json:",omitempty" yaml:",omitempty` 557 558 ch Chan 559 } 560 561 // Substitute bindings for the receiver 562 func (r *Recv) Substitute(ctx *Ctx, t *Test) (*Recv, error) { 563 564 // Canonicalize r.Target. 565 switch r.Target { 566 case "payload", "Payload", "": 567 r.Target = "payload" 568 case "msg", "message", "Message": 569 r.Target = "msg" 570 default: 571 return nil, NewBroken(fmt.Errorf("bad Recv Target: '%s'", r.Target)) 572 } 573 574 t.Bindings.Clean(ctx, r.ClearBindings) 575 576 topic, err := t.Bindings.StringSub(ctx, r.Topic) 577 if err != nil { 578 return nil, err 579 } 580 ctx.Inddf(" Effective topic: %s", topic) 581 582 var pat = r.Pattern 583 var reg = r.Regexp 584 if r.Regexp == "" { 585 // ToDo: Probably go with an explicit 586 // 'PatternSerialization' property. Might also need a 587 // 'MessageSerialization' property, too. Alternately, 588 // rely on regex matching for non-text messages and 589 // patterns. 590 js, err := t.Bindings.SerialSub(ctx, "", r.Pattern) 591 if err != nil { 592 return nil, err 593 } 594 var x interface{} 595 if err = json.Unmarshal([]byte(js), &x); err != nil { 596 // See the ToDo above. If we can't 597 // deserialize, we'll just go with the string 598 // literal. 599 pat = js 600 } else { 601 pat = x 602 } 603 604 ctx.Inddf(" Effective pattern: %s", JSON(pat)) 605 606 } else { 607 if r.Pattern != nil { 608 return nil, Brokenf("can't have both Pattern and Regexp") 609 } 610 if reg, err = t.Bindings.StringSub(ctx, reg); err != nil { 611 return nil, err 612 } 613 ctx.Inddf(" Effective regexp: %s", reg) 614 } 615 616 guard, err := t.Bindings.StringSub(ctx, r.Guard) 617 if err != nil { 618 return nil, err 619 } 620 621 run, err := t.Bindings.StringSub(ctx, r.Run) 622 if err != nil { 623 return nil, err 624 } 625 626 return &Recv{ 627 Chan: r.Chan, 628 Topic: topic, 629 Pattern: pat, 630 Regexp: reg, 631 Timeout: r.Timeout, 632 Target: r.Target, 633 Guard: guard, 634 Run: run, 635 Schema: r.Schema, 636 Attempts: r.Attempts, 637 ch: r.ch, 638 }, nil 639 } 640 641 func validateSchema(ctx *Ctx, schemaURI string, payload string) error { 642 ctx.Indf(" schema: %s", schemaURI) 643 var ( 644 doc = jschema.NewStringLoader(payload) 645 schema = jschema.NewReferenceLoader(schemaURI) 646 ) 647 648 v, err := jschema.Validate(schema, doc) 649 if err != nil { 650 return Brokenf("schema validation error: %v", err) 651 } 652 if !v.Valid() { 653 var ( 654 errs = v.Errors() 655 complaints = make([]string, len(errs)) 656 ) 657 for i, err := range errs { 658 complaints[i] = err.String() 659 ctx.Indf(" schema invalidation: %s", err) 660 } 661 return fmt.Errorf("schema (%s) validation errors: %s", 662 schemaURI, strings.Join(complaints, "; ")) 663 } 664 ctx.Indf(" schema validated") 665 return nil 666 } 667 668 // Exec the receiver 669 func (r *Recv) Exec(ctx *Ctx, t *Test) error { 670 var ( 671 timeout = r.Timeout 672 in = r.ch.Recv(ctx) 673 attempts = 0 674 ) 675 676 if timeout == 0 { 677 timeout = time.Second * 60 * 20 * 24 678 } 679 680 tm := time.NewTimer(timeout) 681 682 if r.Regexp != "" { 683 ctx.Inddf(" Recv regexp %s", r.Regexp) 684 } else { 685 ctx.Inddf(" Recv pattern (%T) %v", r.Pattern, r.Pattern) 686 } 687 688 ctx.Inddf(" Recv target %s", r.Target) 689 for { 690 select { 691 case <-ctx.Done(): 692 ctx.Indf(" Recv canceled") 693 return nil 694 case <-tm.C: 695 ctx.Indf(" Recv timeout (%v)", timeout) 696 return fmt.Errorf("timeout after %s waiting for %s", timeout, r.Pattern) 697 case m := <-in: 698 ctx.Indf(" Recv dequeuing topic '%s' (vs '%s')", m.Topic, r.Topic) 699 ctx.Inddf(" %s", m.Payload) 700 701 var ( 702 err error 703 bss []match.Bindings 704 ) 705 706 // Verify that either no Recv topic was 707 // provided or that the receiver topic is 708 // equal to the message topic 709 if r.Topic == "" || r.Topic == m.Topic { 710 ctx.Indf(" Recv match:") 711 712 if r.Regexp != "" { 713 ctx.Inddf(" regexp: %s", r.Regexp) 714 if r.Target != "payload" { 715 return Brokenf("can only regexp-match against payload (not also topic)") 716 } 717 bss, err = RegexpMatch(r.Regexp, m.Payload) 718 } else { 719 ctx.Inddf(" pattern: %s", JSON(r.Pattern)) 720 721 // target will be the target (message) for matching. 722 var target interface{} 723 if err = json.Unmarshal([]byte(m.Payload), &target); err != nil { 724 return err 725 } 726 727 switch r.Target { 728 case "payload": 729 // Match against only the (deserialized) payload. 730 case "msg": 731 // Match against the full message 732 // (with topic and deserialized 733 // payload). 734 target = map[string]interface{}{ 735 "Topic": m.Topic, 736 "Payload": target, 737 } 738 default: 739 return Brokenf("bad Recv Target: '%s'", r.Target) 740 } 741 742 ctx.Inddf(" match target: %s", JSON(target)) 743 744 if r.Schema != "" { 745 if err := validateSchema(ctx, r.Schema, m.Payload); err != nil { 746 return err 747 } 748 } 749 750 target = Canon(target) 751 t.Bindings.Clean(ctx, r.ClearBindings) 752 pattern, err := t.Bindings.Bind(ctx, r.Pattern) 753 if err != nil { 754 return err 755 } 756 757 ctx.Inddf(" bound pattern: %s", JSON(pattern)) 758 bss, err = match.Match(pattern, target, match.NewBindings()) 759 } 760 761 if err != nil { 762 return err 763 } 764 ctx.Indf(" result: %v", 0 < len(bss)) 765 766 if 0 < len(bss) { 767 768 if 1 < len(bss) { 769 // Let's protest if we get 770 // multiple sets of bindings. 771 // 772 // Better safe than sorry? If 773 // we start running into this 774 // situation, let's figure out 775 // the best way to proceed. 776 // Otherwise we might not notice 777 // unintended behavior. 778 return fmt.Errorf("multiple bindings sets: %s", JSON(bss)) 779 } 780 781 // Extend rather than replace 782 // t.Bindings. Note that we have to 783 // extend t.Bindings rather than replace 784 // it due to the bindings substitution 785 // logic. See the comments above 786 // 'Match' above. 787 // 788 // ToDo: Contemplate possibility for 789 // inconsistencies. 790 // 791 // Thanks, Carlos, for this fix! 792 if t.Bindings == nil { 793 // Some unit tests might not 794 // have initialized t.Bindings. 795 t.Bindings = make(map[string]interface{}) 796 } 797 for p, v := range bss[0] { 798 if x, have := t.Bindings[p]; have { 799 // Let's see if we are 800 // changing an existing 801 // binding. If so, note 802 // that. 803 js0 := JSON(v) 804 js1 := JSON(x) 805 if js0 != js1 { 806 ctx.Indf(" Updating binding for %s", p) 807 } 808 } 809 t.Bindings[p] = v 810 } 811 812 if r.Guard != "" { 813 ctx.Indf(" Recv guard") 814 src, err := t.prepareSource(ctx, r.Guard) 815 if err != nil { 816 return err 817 } 818 819 // Convert bss to a stripped representation ... 820 js, _ := json.Marshal(&bss) 821 var bindingss interface{} 822 json.Unmarshal(js, &bindingss) 823 // And again ... 824 var bs interface{} 825 js, _ = subst.JSONMarshal(&bss[0]) 826 json.Unmarshal(js, &bs) 827 828 env := t.jsEnv(ctx) 829 env["bindingss"] = bindingss 830 env["msg"] = m 831 832 x, err := JSExec(ctx, src, env) 833 if f, is := IsFailure(x); is { 834 return f 835 } 836 if f, is := IsFailure(err); is { 837 return f 838 } 839 if err != nil { 840 return err 841 } 842 843 switch vv := x.(type) { 844 case bool: 845 if !vv { 846 ctx.Indf(" Recv guard not pleased") 847 continue 848 } 849 ctx.Indf(" Recv guard satisfied") 850 default: 851 return Brokenf("Guard Javascript returned a %T (%v) and not a bool", x, x) 852 } 853 } 854 855 ctx.BindingsRedactions(t.Bindings) 856 857 ctx.Indf(" Recv satisfied") 858 ctx.Inddf(" t.Bindings: %s", JSON(t.Bindings)) 859 860 if r.Run != "" { 861 src, err := t.prepareSource(ctx, r.Run) 862 if err != nil { 863 return err 864 } 865 866 // Convert bss to a stripped representation ... 867 env := t.jsEnv(ctx) 868 can := Canon(&bss) 869 env["bindingss"] = can 870 env["bss"] = can 871 env["msg"] = m 872 873 if _, err = JSExec(ctx, src, env); err != nil { 874 return err 875 } 876 } 877 878 return nil 879 } 880 881 // Only increment the number of attempts given a topic match. 882 attempts++ 883 } 884 885 // Verify the receiver attempts was specified (not 0) and that 886 // the actual number of attempts has been reached 887 if r.Attempts != 0 && attempts >= r.Attempts { 888 ctx.Inddf(" attempts: %d of %d", attempts, r.Attempts) 889 ctx.Inddf(" topic: %s", r.Topic) 890 match := fmt.Sprintf("pattern: %s", r.Pattern) 891 if r.Regexp != "" { 892 match = fmt.Sprintf("regexp: %s", r.Regexp) 893 } 894 if r.Topic != "" { 895 return fmt.Errorf("%d attempt(s) reached; expected maximum of %d attempt(s) to match %s on topic %s", attempts, r.Attempts, match, r.Topic) 896 } 897 return fmt.Errorf("%d attempt(s) reached; expected maximum of %d attempt(s) to match %s", attempts, r.Attempts, match) 898 } 899 } 900 } 901 902 return fmt.Errorf("impossible!") 903 } 904 905 type Kill struct { 906 Chan string 907 908 ch Chan 909 } 910 911 func (p *Kill) Substitute(ctx *Ctx, t *Test) (*Kill, error) { 912 return p, nil 913 } 914 915 func (p *Kill) Exec(ctx *Ctx, t *Test) error { 916 ctx.Indf(" Kill %s", JSON(p)) 917 918 return p.ch.Kill(ctx) 919 } 920 921 type Reconnect struct { 922 Chan string 923 924 ch Chan 925 } 926 927 func (p *Reconnect) Substitute(ctx *Ctx, t *Test) (*Reconnect, error) { 928 return p, nil 929 } 930 931 func (p *Reconnect) Exec(ctx *Ctx, t *Test) error { 932 ctx.Indf(" Reconnect %s", JSON(p)) 933 934 return p.ch.Open(ctx) 935 } 936 937 type Close struct { 938 Chan string 939 940 ch Chan 941 } 942 943 func (p *Close) Substitute(ctx *Ctx, t *Test) (*Close, error) { 944 return p, nil 945 } 946 947 func (p *Close) Exec(ctx *Ctx, t *Test) error { 948 ctx.Indf(" Close %s", JSON(p)) 949 950 err := p.ch.Close(ctx) 951 if err == nil { 952 ctx.Indf(" Removing %s", p.Chan) 953 delete(t.Chans, p.Chan) 954 } 955 956 return err 957 } 958 959 type Ingest struct { 960 Chan string 961 Topic string 962 Payload interface{} 963 // Timeout time.Duration 964 965 ch Chan 966 } 967 968 func (i *Ingest) Substitute(ctx *Ctx, t *Test) (*Ingest, error) { 969 topic, err := t.Bindings.StringSub(ctx, i.Topic) 970 if err != nil { 971 return nil, err 972 } 973 974 var pay string 975 if s, is := i.Payload.(string); is { 976 pay = s 977 } else { 978 js, err := subst.JSONMarshal(&i.Payload) 979 if err != nil { 980 return nil, err 981 } 982 pay = string(js) 983 } 984 985 if pay, err = t.Bindings.Sub(ctx, pay); err != nil { 986 return nil, err 987 } 988 989 return &Ingest{ 990 Chan: i.Chan, 991 Topic: topic, 992 Payload: pay, 993 ch: i.ch, 994 }, nil 995 996 } 997 998 func (i *Ingest) Exec(ctx *Ctx, t *Test) error { 999 payload, is := i.Payload.(string) 1000 if !is { 1001 js, err := subst.JSONMarshal(&i.Payload) 1002 if err != nil { 1003 return err 1004 } 1005 payload = string(js) 1006 } 1007 m := Msg{ 1008 Topic: i.Topic, 1009 Payload: payload, 1010 } 1011 1012 return i.ch.To(ctx, m) 1013 } 1014 1015 type Exec struct { 1016 Process 1017 Pattern interface{} 1018 } 1019 1020 func (e *Exec) Exec(ctx *Ctx, t *Test) error { 1021 panic("todo") 1022 } 1023 1024 func CopyBindings(bs map[string]interface{}) map[string]interface{} { 1025 if bs == nil { 1026 return make(map[string]interface{}) 1027 } 1028 acc := make(map[string]interface{}, len(bs)) 1029 for p, v := range bs { 1030 acc[p] = v 1031 } 1032 return acc 1033 } 1034 1035 func (t *Test) jsEnv(ctx *Ctx) map[string]interface{} { 1036 bs := CopyBindings(t.Bindings) 1037 return map[string]interface{}{ 1038 "bindings": bs, 1039 "bs": bs, 1040 "test": t, 1041 "elapsed": float64(t.elapsed) / 1000 / 1000, // Milliseconds 1042 } 1043 }