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  }