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 }