github.com/Comcast/plax@v0.8.32/dsl/test.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  	"fmt"
    23  	"io/ioutil"
    24  	"strings"
    25  	"time"
    26  )
    27  
    28  var (
    29  	DefaultMaxSteps = 100
    30  )
    31  
    32  // Test is the top-level type for a complete test.
    33  type Test struct {
    34  	// Id usually comes from the filename that defines the test.
    35  	Id string `json:",omitempty" yaml:",omitempty"`
    36  
    37  	// Name is the test specification name
    38  	Name string `json:",omitempty" yaml:",omitempty"`
    39  
    40  	// Doc is an optional documentation string.
    41  	Doc string `json:",omitempty" yaml:",omitempty"`
    42  
    43  	// Labels is an optional set of labels (e.g., "cpe", "app").
    44  	Labels []string `json:",omitempty" yaml:",omitempty"`
    45  
    46  	// Priority 0 is the highest priority.
    47  	Priority int
    48  
    49  	// Spec is the test specification.
    50  	//
    51  	// Parts of the Spec are subject to bindings substitution.
    52  	Spec *Spec
    53  
    54  	// State is arbitrary state the Javascript code can use.
    55  	State map[string]interface{}
    56  
    57  	// Bindings is the first set of bindings returned by the last
    58  	// pattern match (if any).
    59  	Bindings Bindings
    60  
    61  	// Chans is the map of Chan names to Chans.
    62  	Chans map[string]Chan
    63  
    64  	// T is the time the last Step executed.
    65  	T time.Time
    66  
    67  	// Optional seed for random number generator.
    68  	//
    69  	// Effectively defaults to the current time in UNIX
    70  	// nanoseconds
    71  	Seed int64
    72  
    73  	// MaxSteps, when not zero, is the maximum number of steps to
    74  	// execute.
    75  	//
    76  	// Can act as a circuit breaker for infinite loops due to
    77  	// branches.
    78  	MaxSteps int
    79  
    80  	// Libraries is a list of filenames that should contain
    81  	// Javascript.  This source is loaded into each Javascript
    82  	// environment.
    83  	//
    84  	// Warning: These files are loaded for each Javascript
    85  	// invocation (because re-using the Javascript environment is
    86  	// not a safe thing to do -- and I don't think I can "seal"
    87  	// one environment and extend it per-invocation).
    88  	Libraries []string
    89  
    90  	// Negative indicates that a reported failure (but not error)
    91  	// should be interpreted as a success.
    92  	Negative bool
    93  
    94  	// elapsed is duration between the most recent steps.
    95  	elapsed time.Duration
    96  
    97  	// Dir is the base directory for reading relative pathnames
    98  	// (for libraries, includes, and ##FILENAMEs).
    99  	Dir string
   100  
   101  	// Retries is an optional retry specification.
   102  	//
   103  	// This data isn't actually used in the code here.  Instead,
   104  	// this data is here to make it easy for a test to specify its
   105  	// own retry policy (if any).  Actual implementation provided
   106  	// by invoke.Run().
   107  	Retries *Retries
   108  
   109  	// Registry is the channel (type) registry for this test.
   110  	//
   111  	// Defaults to TheChanRegistry.
   112  	Registry ChanRegistry
   113  }
   114  
   115  // NewTest create a initialized NewTest from the id and Spec
   116  func NewTest(ctx *Ctx, id string, s *Spec) *Test {
   117  	return &Test{
   118  		Id:       id,
   119  		Spec:     s,
   120  		Chans:    make(map[string]Chan),
   121  		State:    make(map[string]interface{}),
   122  		Bindings: make(map[string]interface{}),
   123  		MaxSteps: DefaultMaxSteps,
   124  		T:        time.Now().UTC(),
   125  	}
   126  }
   127  
   128  // Wanted reports whether a test meets the given requirements.
   129  func (t *Test) Wanted(ctx *Ctx, lowestPriority int, labels []string, tests []string) bool {
   130  	if 0 <= lowestPriority && t.Priority > lowestPriority {
   131  		return false
   132  	}
   133  LABELS:
   134  	for _, label := range labels {
   135  		if label == "" {
   136  			continue
   137  		}
   138  		for _, have := range t.Labels {
   139  			if label == have {
   140  				continue LABELS
   141  			}
   142  		}
   143  		return false
   144  	}
   145  
   146  	// Iterate over specified suite tests to see if they are wanted
   147  	for _, name := range tests {
   148  		if t.Name == name {
   149  			// Suite tests is wanted
   150  			return true
   151  		}
   152  	}
   153  	// Reaching here means the suite test was not wanted
   154  	if len(tests) > 0 {
   155  		return false
   156  	}
   157  
   158  	return true
   159  }
   160  
   161  // Tick returns the duration since the last Tick.
   162  func (t *Test) Tick(ctx *Ctx) time.Duration {
   163  	now := time.Now().UTC()
   164  	t.elapsed = now.Sub(t.T)
   165  	t.T = now
   166  	return t.elapsed
   167  }
   168  
   169  // HappyTerminalPhases is the set of phase names that indicate that
   170  // the test has completed successfully.
   171  var HappyTerminalPhases = []string{"", "happy", "done"}
   172  
   173  // HappyTerminalPhase reports whether the given phase name represents
   174  // a happy terminal phase.
   175  func HappyTerminalPhase(name string) bool {
   176  	for _, s := range HappyTerminalPhases {
   177  		if s == name {
   178  			return true
   179  		}
   180  	}
   181  	return false
   182  }
   183  
   184  // Errors collects errors from the main test as well as from final
   185  // phase executions.
   186  type Errors struct {
   187  	InitErr     error
   188  	Err         error
   189  	FinalErrors map[string]error
   190  }
   191  
   192  // NewErrors does what you expect.
   193  func NewErrors() *Errors {
   194  	return &Errors{
   195  		FinalErrors: make(map[string]error),
   196  	}
   197  }
   198  
   199  // IsFine reports whether all errors are actually nil.
   200  func (es *Errors) IsFine() bool {
   201  	if es == nil {
   202  		return true
   203  	}
   204  
   205  	if es.InitErr != nil || es.Err != nil {
   206  		return false
   207  	}
   208  
   209  	for _, err := range es.FinalErrors {
   210  		if err != nil {
   211  			return false
   212  		}
   213  	}
   214  
   215  	return true
   216  }
   217  
   218  // IsBroken is a possibly useless method that reports the first Broken
   219  // error (if any).
   220  //
   221  // Also see the function IsBroken.
   222  func (es *Errors) IsBroken() (*Broken, bool) {
   223  	b := &Broken{
   224  		Err: es,
   225  	}
   226  
   227  	if _, is := IsBroken(es.InitErr); is {
   228  		return b, true
   229  	}
   230  
   231  	if _, is := IsBroken(es.Err); is {
   232  		return b, true
   233  	}
   234  
   235  	for _, err := range es.FinalErrors {
   236  		if _, is := IsBroken(err); is {
   237  			return b, true
   238  		}
   239  	}
   240  
   241  	return nil, false
   242  }
   243  
   244  // Errors instances are errors.
   245  func (es *Errors) Error() string {
   246  	var acc string
   247  	if es.InitErr != nil {
   248  		acc = "InitErr: " + es.Err.Error()
   249  	}
   250  
   251  	if es.Err != nil {
   252  		if 0 < len(acc) {
   253  			acc += "; "
   254  		}
   255  		acc = "Err: " + es.Err.Error()
   256  	}
   257  
   258  	for phase, err := range es.FinalErrors {
   259  		if err == nil {
   260  			continue
   261  		}
   262  		if 0 < len(acc) {
   263  			acc += "; "
   264  		}
   265  		acc += "final " + phase + ": " + err.Error()
   266  	}
   267  
   268  	return acc
   269  }
   270  
   271  // Run initializes the Mother channel, runs the test, and runs final
   272  // phases (if any).
   273  func (t *Test) Run(ctx *Ctx) *Errors {
   274  
   275  	errs := NewErrors()
   276  
   277  	if err := t.InitChans(ctx); err != nil {
   278  		errs.InitErr = err
   279  		return errs
   280  	}
   281  
   282  	// Run the main sequence.
   283  
   284  	from := t.Spec.InitialPhase
   285  	if from == "" {
   286  		from = DefaultInitialPhase
   287  	}
   288  
   289  	errs.Err = t.RunFrom(ctx, from)
   290  
   291  	// Run the final phases.
   292  
   293  	for _, phase := range t.Spec.FinalPhases {
   294  		if e := t.RunFrom(ctx, phase); e != nil {
   295  			errs.FinalErrors[phase] = e
   296  		}
   297  	}
   298  
   299  	if !errs.IsFine() {
   300  		return errs
   301  	}
   302  
   303  	return nil
   304  }
   305  
   306  // bindingRedactions adds redaction patterns for values of binding
   307  // variables that start with X_.
   308  func (t *Test) bindingRedactions(ctx *Ctx) error {
   309  	return ctx.BindingsRedactions(t.Bindings)
   310  }
   311  
   312  // RunFrom begins test execution starting at the given phase.
   313  func (t *Test) RunFrom(ctx *Ctx, from string) error {
   314  	stepsTaken := 0
   315  	for {
   316  		if err := t.bindingRedactions(ctx); err != nil {
   317  			return err
   318  		}
   319  		p, have := t.Spec.Phases[from]
   320  		if !have {
   321  			return fmt.Errorf("No phase '%s'", from)
   322  		}
   323  		ctx.Indf("Phase %s", from)
   324  
   325  		next, err := p.Exec(ctx, t)
   326  		if err != nil {
   327  			_, broke := IsBroken(err)
   328  			err := fmt.Errorf("phase %s: %w", from, err)
   329  			if broke {
   330  				return NewBroken(err)
   331  			} else {
   332  				return err
   333  			}
   334  		}
   335  
   336  		stepsTaken++
   337  		if 0 < t.MaxSteps && t.MaxSteps <= stepsTaken {
   338  			return fmt.Errorf("MaxSteps (%d) reached", t.MaxSteps)
   339  		}
   340  
   341  		if HappyTerminalPhase(next) {
   342  			return nil
   343  		}
   344  
   345  		from = next
   346  	}
   347  }
   348  
   349  func (t *Test) makeChan(ctx *Ctx, kind ChanKind, opts interface{}) (Chan, error) {
   350  	if t.Registry == nil {
   351  		t.Registry = TheChanRegistry
   352  	}
   353  
   354  	maker, have := t.Registry[kind]
   355  	if !have {
   356  		return nil, fmt.Errorf("unknown Chan kind: '%s'", kind)
   357  	}
   358  
   359  	var x interface{}
   360  	if err := t.Bindings.SubX(ctx, opts, &x); err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	return maker(ctx, x)
   365  }
   366  
   367  // Validate executes a few sanity checks.
   368  //
   369  // Should probably be called after Init().
   370  func (t *Test) Validate(ctx *Ctx) []error {
   371  	errs := make([]error, 0, 8)
   372  
   373  	// Check that each step has exactly one operation.
   374  	for name, p := range t.Spec.Phases {
   375  		for i, s := range p.Steps {
   376  			ops := 0
   377  			if s.Pub != nil {
   378  				ops++
   379  			}
   380  			if s.Sub != nil {
   381  				ops++
   382  			}
   383  			if s.Recv != nil {
   384  				ops++
   385  			}
   386  			if s.Goto != "" {
   387  				ops++
   388  			}
   389  			if s.Ingest != nil {
   390  				ops++
   391  			}
   392  			if s.Kill != nil {
   393  				ops++
   394  			}
   395  			if s.Run != "" {
   396  				ops++
   397  			}
   398  			if s.Reconnect != nil {
   399  				ops++
   400  			}
   401  			if s.Close != nil {
   402  				ops++
   403  			}
   404  			if s.Wait != "" {
   405  				ops++
   406  			}
   407  			if s.Branch != "" {
   408  				ops++
   409  			}
   410  			if s.Doc != "" {
   411  				ops++
   412  			}
   413  			if ops != 1 {
   414  				errs = append(errs,
   415  					fmt.Errorf("Step %d of phase %s does not have exactly one ops (%d)",
   416  						i, name, ops))
   417  			}
   418  		}
   419  	}
   420  
   421  	// Check that any Goto Step is the last step in a Phase.
   422  	//
   423  	// ToDo: Maybe require all Phases to have Goto.
   424  	for name, p := range t.Spec.Phases {
   425  		for i := 0; i < len(p.Steps)-1; i++ {
   426  			s := p.Steps[i]
   427  			if s.Goto != "" {
   428  				errs = append(errs,
   429  					fmt.Errorf("Goto step %d in phase '%s' is not the last step",
   430  						i, name))
   431  			}
   432  		}
   433  	}
   434  
   435  	// Check that each Goto Step has a defined Phase.
   436  	for phaseName, p := range t.Spec.Phases {
   437  		for i, s := range p.Steps {
   438  			if s.Goto == "" {
   439  				continue
   440  			}
   441  			if HappyTerminalPhase(s.Goto) {
   442  				continue
   443  			}
   444  			if _, have := t.Spec.Phases[s.Goto]; !have {
   445  				errs = append(errs,
   446  					fmt.Errorf("No phase '%s', which is targeted by step %d in phase '%s'",
   447  						s.Goto, i, phaseName))
   448  			}
   449  		}
   450  	}
   451  	if len(errs) == 0 {
   452  		return nil
   453  	}
   454  
   455  	return errs
   456  }
   457  
   458  func (t *Test) Init(ctx *Ctx) error {
   459  	// Previously we parsed Wait strings here, but that approach
   460  	// was wrong because is wouldn't work for late bindings
   461  	// subsitution.  So we delay parsing until Wait execution
   462  	// time.
   463  
   464  	return nil
   465  }
   466  
   467  func (t *Test) InitChans(ctx *Ctx) error {
   468  	ctx.Indf("InitChans")
   469  
   470  	m, err := NewMother(ctx, nil)
   471  	if err != nil {
   472  		return err
   473  	}
   474  	m.t = t
   475  	if t.Chans == nil {
   476  		t.Chans = make(map[string]Chan)
   477  	}
   478  	t.Chans["mother"] = m
   479  
   480  	return nil
   481  }
   482  
   483  func (t *Test) ensureChan(ctx *Ctx, name string, dst *Chan) error {
   484  
   485  	if name == "" {
   486  
   487  		switch len(t.Chans) {
   488  		case 0:
   489  			// Internal error: Should always have mother.
   490  			return fmt.Errorf("channel name is empty and can't find a default")
   491  		case 1:
   492  			for s, _ := range t.Chans {
   493  				name = s
   494  				break
   495  			}
   496  		case 2:
   497  			for s, _ := range t.Chans {
   498  				if s != "mother" {
   499  					name = s
   500  					break
   501  				}
   502  			}
   503  			if name == "" {
   504  				return fmt.Errorf("channel name is empty and can't find a default")
   505  			}
   506  		default:
   507  			return fmt.Errorf("channel name is empty and can't choose a default")
   508  		}
   509  		ctx.Indf("    chan: %s", name)
   510  
   511  	}
   512  
   513  	c, have := t.Chans[name]
   514  	if !have {
   515  		return fmt.Errorf("no channel named '%s'", name)
   516  	}
   517  
   518  	if c == nil {
   519  		// Internal error?
   520  		return fmt.Errorf("channel named '%s' is nil", name)
   521  	}
   522  
   523  	*dst = c
   524  
   525  	return nil
   526  }
   527  
   528  func (t *Test) Close(ctx *Ctx) error {
   529  	for _, c := range t.Chans {
   530  		if err := c.Close(ctx); err != nil {
   531  			return err
   532  		}
   533  	}
   534  	return nil
   535  }
   536  
   537  func TestIdFromPathname(s string) string {
   538  	for _, suffix := range []string{"yaml", "json"} {
   539  		if strings.HasSuffix(s, "."+suffix) {
   540  			i := len(s) - len(suffix) - 1
   541  			return s[0:i]
   542  		}
   543  	}
   544  	return s
   545  }
   546  
   547  func (t *Test) getLibraries(ctx *Ctx) (string, error) {
   548  	var src string
   549  	for _, filename := range t.Libraries {
   550  		filename = t.Dir + "/" + filename
   551  		js, err := ioutil.ReadFile(filename)
   552  		if err != nil {
   553  			return "", fmt.Errorf("error reading library '%s': %w", filename, err)
   554  		}
   555  		src += fmt.Sprintf("// library: %s\n\n", filename) + string(js) + "\n"
   556  	}
   557  	return src, nil
   558  }
   559  
   560  func (t *Test) prepareSource(ctx *Ctx, code string) (string, error) {
   561  	libs, err := t.getLibraries(ctx)
   562  	if err != nil {
   563  		return "", err
   564  	}
   565  	src := libs + "\n" + fmt.Sprintf("(function()\n{\n%s\n})()", code)
   566  	return src, nil
   567  }
   568  
   569  // Bind replaces all bindings in the given (structured) thing.
   570  func (t *Test) Bind(ctx *Ctx, x interface{}) (interface{}, error) {
   571  	return t.Bindings.Bind(ctx, x)
   572  }
   573  
   574  // Retries represents a specification for how to retry a failed test.
   575  type Retries struct {
   576  	// N is the maximum number of retries.
   577  	N int
   578  
   579  	// Delay is the initial delay before the first retry.
   580  	Delay time.Duration
   581  
   582  	// DelayFactor is multiplied by the last delay to return the
   583  	// next delay.
   584  	DelayFactor float64
   585  }
   586  
   587  // NewRetries returns the default Retries specification.  N is 0.
   588  func NewRetries() *Retries {
   589  	return &Retries{
   590  		N:           0,           // By default, no retries.
   591  		Delay:       time.Second, // I guess.
   592  		DelayFactor: 2,           // 1, 2, 4, 8, 16, 32, ...
   593  	}
   594  }
   595  
   596  // NextDelay multiplies the given delay by r.DelayFactor.
   597  func (r *Retries) NextDelay(d time.Duration) time.Duration {
   598  	return time.Duration(float64(d) * r.DelayFactor)
   599  }