From 45911fe6c2754bfb76b1960b57cee7d7d3831cd5 Mon Sep 17 00:00:00 2001 From: Jamie Stephens Date: Fri, 9 Jul 2021 15:54:48 +0000 Subject: [PATCH 1/2] Break up Recv.Exec into reasonable parts --- dsl/ctx.go | 4 + dsl/spec.go | 419 +---------------------------------------------- dsl/spec_test.go | 17 -- 3 files changed, 6 insertions(+), 434 deletions(-) diff --git a/dsl/ctx.go b/dsl/ctx.go index a876646..241a70f 100644 --- a/dsl/ctx.go +++ b/dsl/ctx.go @@ -243,3 +243,7 @@ type GoLogger struct { func (l *GoLogger) Printf(format string, args ...interface{}) { log.Printf(format, args...) } + +func (c *Ctx) quiet() { + c.LogLevel = "NONE" +} diff --git a/dsl/spec.go b/dsl/spec.go index 9bbe291..2fa600a 100644 --- a/dsl/spec.go +++ b/dsl/spec.go @@ -19,14 +19,10 @@ package dsl import ( - "encoding/json" "fmt" - "strings" "time" "github.com/Comcast/plax/subst" - "github.com/Comcast/sheens/match" - jschema "github.com/xeipuuv/gojsonschema" ) var DefaultInitialPhase = "phase1" @@ -473,417 +469,6 @@ func (s *Sub) Exec(ctx *Ctx, t *Test) error { return s.ch.Sub(ctx, s.Topic) } -type Recv struct { - Chan string - Topic string - - // Pattern is a Sheens pattern - // https://github.com/Comcast/sheens/blob/main/README.md#pattern-matching - // for matching incoming messages. - // - // Use a pattern for matching JSON-serialized messages. - // - // Also see Regexp. - Pattern interface{} - - // Regexp, which is an alternative to Pattern, gives a (Go) - // regular expression used to match incoming messages. - // - // A named group match becomes a bound variable. - Regexp string - - Timeout time.Duration - - // Target is an optional switch to specify what part of the - // incoming message is considered for matching. - // - // By default, only the payload is matched. If Target is - // "message", then matching is performed against - // - // {"Topic":TOPIC,"Payload":PAYLOAD} - // - // which allows matching based on the topic of in-bound - // messages. - Target string - - // ClearBindings will remove all bindings for variables that - // do not start with '?!' before executing this step. - ClearBindings bool - - // Guard is optional Javascript (!) that should return a - // boolean to indicate whether this Recv has been satisfied. - // - // The code is executed in a function body, and the code - // should 'return' a boolean. - // - // The following variables are bound in the global - // environment: - // - // bindingss: the set (array) of bindings returned by match() - // - // elapsed: the elapsed time in milliseconds since the last step - // - // msg: the receved message ({"topic":TOPIC,"payload":PAYLOAD}) - // - // print: a function that prints its arguments to stdout. - // - Guard string `json:",omitempty" yaml:",omitempty"` - - Run string `json:",omitempty" yaml:",omitempty"` - - // Schema is an optional URI for a JSON Schema that's used to - // validate incoming messages before other processing. - Schema string `json:",omitempty" yaml:",omitempty"` - - // Max attempts to receive a message; optionally for a specific topic - Attempts int `json:",omitempty" yaml:",omitempty` - - ch Chan -} - -// Substitute bindings for the receiver -func (r *Recv) Substitute(ctx *Ctx, t *Test) (*Recv, error) { - - // Canonicalize r.Target. - switch r.Target { - case "payload", "Payload", "": - r.Target = "payload" - case "msg", "message", "Message": - r.Target = "msg" - default: - return nil, NewBroken(fmt.Errorf("bad Recv Target: '%s'", r.Target)) - } - - t.Bindings.Clean(ctx, r.ClearBindings) - - topic, err := t.Bindings.StringSub(ctx, r.Topic) - if err != nil { - return nil, err - } - ctx.Inddf(" Effective topic: %s", topic) - - var pat = r.Pattern - var reg = r.Regexp - if r.Regexp == "" { - // ToDo: Probably go with an explicit - // 'PatternSerialization' property. Might also need a - // 'MessageSerialization' property, too. Alternately, - // rely on regex matching for non-text messages and - // patterns. - js, err := t.Bindings.SerialSub(ctx, "", r.Pattern) - if err != nil { - return nil, err - } - var x interface{} - if err = json.Unmarshal([]byte(js), &x); err != nil { - // See the ToDo above. If we can't - // deserialize, we'll just go with the string - // literal. - pat = js - } else { - pat = x - } - - ctx.Inddf(" Effective pattern: %s", JSON(pat)) - - } else { - if r.Pattern != nil { - return nil, Brokenf("can't have both Pattern and Regexp") - } - if reg, err = t.Bindings.StringSub(ctx, reg); err != nil { - return nil, err - } - ctx.Inddf(" Effective regexp: %s", reg) - } - - guard, err := t.Bindings.StringSub(ctx, r.Guard) - if err != nil { - return nil, err - } - - run, err := t.Bindings.StringSub(ctx, r.Run) - if err != nil { - return nil, err - } - - return &Recv{ - Chan: r.Chan, - Topic: topic, - Pattern: pat, - Regexp: reg, - Timeout: r.Timeout, - Target: r.Target, - Guard: guard, - Run: run, - Schema: r.Schema, - Attempts: r.Attempts, - ch: r.ch, - }, nil -} - -func validateSchema(ctx *Ctx, schemaURI string, payload string) error { - ctx.Indf(" schema: %s", schemaURI) - var ( - doc = jschema.NewStringLoader(payload) - schema = jschema.NewReferenceLoader(schemaURI) - ) - - v, err := jschema.Validate(schema, doc) - if err != nil { - return Brokenf("schema validation error: %v", err) - } - if !v.Valid() { - var ( - errs = v.Errors() - complaints = make([]string, len(errs)) - ) - for i, err := range errs { - complaints[i] = err.String() - ctx.Indf(" schema invalidation: %s", err) - } - return fmt.Errorf("schema (%s) validation errors: %s", - schemaURI, strings.Join(complaints, "; ")) - } - ctx.Indf(" schema validated") - return nil -} - -// Exec the receiver -func (r *Recv) Exec(ctx *Ctx, t *Test) error { - var ( - timeout = r.Timeout - in = r.ch.Recv(ctx) - attempts = 0 - ) - - if timeout == 0 { - timeout = time.Second * 60 * 20 * 24 - } - - tm := time.NewTimer(timeout) - - if r.Regexp != "" { - ctx.Inddf(" Recv regexp %s", r.Regexp) - } else { - ctx.Inddf(" Recv pattern (%T) %v", r.Pattern, r.Pattern) - } - - ctx.Inddf(" Recv target %s", r.Target) - for { - select { - case <-ctx.Done(): - ctx.Indf(" Recv canceled") - return nil - case <-tm.C: - ctx.Indf(" Recv timeout (%v)", timeout) - return fmt.Errorf("timeout after %s waiting for %s", timeout, r.Pattern) - case m := <-in: - - ctx.Indf(" Recv dequeuing topic '%s' (vs '%s')", m.Topic, r.Topic) - ctx.Inddf(" %s", m.Payload) - - var ( - err error - bss []match.Bindings - ) - - // Verify that either no Recv topic was - // provided or that the receiver topic is - // equal to the message topic - if r.Topic == "" || r.Topic == m.Topic { - ctx.Indf(" Recv match:") - - if r.Regexp != "" { - ctx.Inddf(" regexp: %s", r.Regexp) - if r.Target != "payload" { - return Brokenf("can only regexp-match against payload (not also topic)") - } - bss, err = RegexpMatch(r.Regexp, m.Payload) - } else { - ctx.Inddf(" pattern: %s", JSON(r.Pattern)) - - // target will be the target (message) for matching. - var target interface{} - if err = json.Unmarshal([]byte(m.Payload), &target); err != nil { - return err - } - - switch r.Target { - case "payload": - // Match against only the (deserialized) payload. - case "msg": - // Match against the full message - // (with topic and deserialized - // payload). - target = map[string]interface{}{ - "Topic": m.Topic, - "Payload": target, - } - default: - return Brokenf("bad Recv Target: '%s'", r.Target) - } - - ctx.Inddf(" match target: %s", JSON(target)) - - if r.Schema != "" { - if err := validateSchema(ctx, r.Schema, m.Payload); err != nil { - return err - } - } - - target = Canon(target) - t.Bindings.Clean(ctx, r.ClearBindings) - pattern, err := t.Bindings.Bind(ctx, r.Pattern) - if err != nil { - return err - } - ctx.Inddf(" bound pattern: %s", JSON(pattern)) - bss, err = match.Match(pattern, target, match.NewBindings()) - } - - if err != nil { - return err - } - ctx.Indf(" result: %v", 0 < len(bss)) - ctx.Inddf(" bss: %s", JSON(bss)) - - if 0 < len(bss) { - - if 1 < len(bss) { - // Let's protest if we get - // multiple sets of bindings. - // - // Better safe than sorry? If - // we start running into this - // situation, let's figure out - // the best way to proceed. - // Otherwise we might not notice - // unintended behavior. - return fmt.Errorf("multiple bindings sets: %s", JSON(bss)) - } - - // Extend rather than replace - // t.Bindings. Note that we have to - // extend t.Bindings rather than replace - // it due to the bindings substitution - // logic. See the comments above - // 'Match' above. - // - // ToDo: Contemplate possibility for - // inconsistencies. - // - // Thanks, Carlos, for this fix! - if t.Bindings == nil { - // Some unit tests might not - // have initialized t.Bindings. - t.Bindings = make(map[string]interface{}) - } - for p, v := range bss[0] { - if x, have := t.Bindings[p]; have { - // Let's see if we are - // changing an existing - // binding. If so, note - // that. - js0 := JSON(v) - js1 := JSON(x) - if js0 != js1 { - ctx.Indf(" Updating binding for %s", p) - } - } - t.Bindings[p] = v - } - - if r.Guard != "" { - ctx.Indf(" Recv guard") - src, err := t.prepareSource(ctx, r.Guard) - if err != nil { - return err - } - - // Convert bss to a stripped representation ... - js, _ := json.Marshal(&bss) - var bindingss interface{} - json.Unmarshal(js, &bindingss) - // And again ... - var bs interface{} - js, _ = subst.JSONMarshal(&bss[0]) - json.Unmarshal(js, &bs) - - env := t.jsEnv(ctx) - env["bindingss"] = bindingss - env["msg"] = m - - x, err := JSExec(ctx, src, env) - if f, is := IsFailure(x); is { - return f - } - if f, is := IsFailure(err); is { - return f - } - if err != nil { - return err - } - - switch vv := x.(type) { - case bool: - if !vv { - ctx.Indf(" Recv guard not pleased") - continue - } - ctx.Indf(" Recv guard satisfied") - default: - return Brokenf("Guard Javascript returned a %T (%v) and not a bool", x, x) - } - } - - ctx.Indf(" Recv satisfied") - ctx.Inddf(" t.Bindings: %s", JSON(t.Bindings)) - - if r.Run != "" { - src, err := t.prepareSource(ctx, r.Run) - if err != nil { - return err - } - - // Convert bss to a stripped representation ... - env := t.jsEnv(ctx) - can := Canon(&bss) - env["bindingss"] = can - env["bss"] = can - env["msg"] = m - - if _, err = JSExec(ctx, src, env); err != nil { - return err - } - } - - return nil - } - - // Only increment the number of attempts given a topic match. - attempts++ - } - - // Verify the receiver attempts was specified (not 0) and that - // the actual number of attempts has been reached - if r.Attempts != 0 && attempts >= r.Attempts { - ctx.Inddf(" attempts: %d of %d", attempts, r.Attempts) - ctx.Inddf(" topic: %s", r.Topic) - match := fmt.Sprintf("pattern: %s", r.Pattern) - if r.Regexp != "" { - match = fmt.Sprintf("regexp: %s", r.Regexp) - } - if r.Topic != "" { - 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) - } - return fmt.Errorf("%d attempt(s) reached; expected maximum of %d attempt(s) to match %s", attempts, r.Attempts, match) - } - } - } - - return fmt.Errorf("impossible!") -} - type Kill struct { Chan string @@ -995,8 +580,8 @@ func CopyBindings(bs map[string]interface{}) map[string]interface{} { func (t *Test) jsEnv(ctx *Ctx) map[string]interface{} { bs := CopyBindings(t.Bindings) return map[string]interface{}{ - "bindings": bs, - "bs": bs, + "bindings": Canon(bs), + "bs": Canon(bs), "test": t, "elapsed": float64(t.elapsed) / 1000 / 1000, // Milliseconds } diff --git a/dsl/spec_test.go b/dsl/spec_test.go index e4b6a55..5a3033c 100644 --- a/dsl/spec_test.go +++ b/dsl/spec_test.go @@ -179,20 +179,3 @@ func TestFails(t *testing.T) { run(t, ctx, tst) } - -func TestValidateSchema(t *testing.T) { - ctx := NewCtx(nil) - schema := "file://../demos/order.json" - t.Run("happy", func(t *testing.T) { - msg := `{"want":"chips"}` - if err := validateSchema(ctx, schema, msg); err != nil { - t.Fatal(err) - } - }) - t.Run("sad", func(t *testing.T) { - msg := `{"need":"queso"}` - if err := validateSchema(ctx, schema, msg); err == nil { - t.Fatal("'want' was required") - } - }) -} From 3c6745f1a22d12947fc229eebd4c9edb0b12f868 Mon Sep 17 00:00:00 2001 From: Jamie Stephens Date: Fri, 9 Jul 2021 16:08:06 +0000 Subject: [PATCH 2/2] Probably should have included these files --- dsl/recv.go | 488 +++++++++++++++++++++++++++++++++++++++++++++++ dsl/recv_test.go | 283 +++++++++++++++++++++++++++ 2 files changed, 771 insertions(+) create mode 100644 dsl/recv.go create mode 100644 dsl/recv_test.go diff --git a/dsl/recv.go b/dsl/recv.go new file mode 100644 index 0000000..ea0d54a --- /dev/null +++ b/dsl/recv.go @@ -0,0 +1,488 @@ +package dsl + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/Comcast/sheens/match" + jschema "github.com/xeipuuv/gojsonschema" +) + +// Recv is a major Step that checks an in-coming message. +type Recv struct { + Chan string + Topic string + + // Pattern is a Sheens pattern + // https://github.com/Comcast/sheens/blob/main/README.md#pattern-matching + // for matching incoming messages. + // + // Use a pattern for matching JSON-serialized messages. + // + // Also see Regexp. + Pattern interface{} + + // Regexp, which is an alternative to Pattern, gives a (Go) + // regular expression used to match incoming messages. + // + // A named group match becomes a bound variable. + Regexp string + + Timeout time.Duration + + // Target is an optional switch to specify what part of the + // incoming message is considered for matching. + // + // By default, only the payload is matched. If Target is + // "message", then matching is performed against + // + // {"Topic":TOPIC,"Payload":PAYLOAD} + // + // which allows matching based on the topic of in-bound + // messages. + Target string + + // ClearBindings will remove all bindings for variables that + // do not start with '?!' before executing this step. + ClearBindings bool + + // Guard is optional Javascript (!) that should return a + // boolean to indicate whether this Recv has been satisfied. + // + // The code is executed in a function body, and the code + // should 'return' a boolean. + // + // The following variables are bound in the global + // environment: + // + // bindingss: the set (array) of bindings returned by match() + // + // elapsed: the elapsed time in milliseconds since the last step + // + // msg: the receved message ({"topic":TOPIC,"payload":PAYLOAD}) + // + // print: a function that prints its arguments to stdout. + // + Guard string `json:",omitempty" yaml:",omitempty"` + + Run string `json:",omitempty" yaml:",omitempty"` + + // Schema is an optional URI for a JSON Schema that's used to + // validate incoming messages before other processing. + Schema string `json:",omitempty" yaml:",omitempty"` + + // Max attempts to receive a message; optionally for a specific topic + Attempts int `json:",omitempty" yaml:",omitempty` + + ch Chan +} + +// Substitute bindings for the Recv. +// +// Returns a new Recv. +func (r *Recv) Substitute(ctx *Ctx, t *Test) (*Recv, error) { + + // Canonicalize r.Target. + switch r.Target { + case "payload", "Payload", "": + r.Target = "payload" + case "msg", "message", "Message": + r.Target = "msg" + default: + return nil, NewBroken(fmt.Errorf("bad Recv Target: '%s'", r.Target)) + } + + t.Bindings.Clean(ctx, r.ClearBindings) + + topic, err := t.Bindings.StringSub(ctx, r.Topic) + if err != nil { + return nil, err + } + ctx.Inddf(" Effective topic: %s", topic) + + var pat = r.Pattern + var reg = r.Regexp + if r.Regexp == "" { + // ToDo: Probably go with an explicit + // 'PatternSerialization' property. Might also need a + // 'MessageSerialization' property, too. Alternately, + // rely on regex matching for non-text messages and + // patterns. + js, err := t.Bindings.SerialSub(ctx, "", r.Pattern) + if err != nil { + return nil, err + } + var x interface{} + if err = json.Unmarshal([]byte(js), &x); err != nil { + // See the ToDo above. If we can't + // deserialize, we'll just go with the string + // literal. + pat = js + } else { + pat = x + } + + ctx.Inddf(" Effective pattern: %s", JSON(pat)) + + } else { + if r.Pattern != nil { + return nil, Brokenf("can't have both Pattern and Regexp") + } + if reg, err = t.Bindings.StringSub(ctx, reg); err != nil { + return nil, err + } + ctx.Inddf(" Effective regexp: %s", reg) + } + + guard, err := t.Bindings.StringSub(ctx, r.Guard) + if err != nil { + return nil, err + } + + run, err := t.Bindings.StringSub(ctx, r.Run) + if err != nil { + return nil, err + } + + return &Recv{ + Chan: r.Chan, + Topic: topic, + Pattern: pat, + Regexp: reg, + Timeout: r.Timeout, + Target: r.Target, + Guard: guard, + Run: run, + Schema: r.Schema, + Attempts: r.Attempts, + ch: r.ch, + }, nil +} + +// validateSchema checks that the payload has schema at the given URI. +func validateSchema(ctx *Ctx, schemaURI string, payload string) error { + ctx.Indf(" schema: %s", schemaURI) + var ( + doc = jschema.NewStringLoader(payload) + schema = jschema.NewReferenceLoader(schemaURI) + ) + + v, err := jschema.Validate(schema, doc) + if err != nil { + return Brokenf("schema validation error: %v", err) + } + if !v.Valid() { + var ( + errs = v.Errors() + complaints = make([]string, len(errs)) + ) + for i, err := range errs { + complaints[i] = err.String() + ctx.Indf(" schema invalidation: %s", err) + } + return fmt.Errorf("schema (%s) validation errors: %s", + schemaURI, strings.Join(complaints, "; ")) + } + ctx.Indf(" schema validated") + return nil +} + +// attemptMatch performs either pattern- or regex-based matching on +// the incoming message. +func (r *Recv) attemptMatch(ctx *Ctx, t *Test, m Msg) ([]match.Bindings, error) { + // target will be the target (message) for matching. + var target interface{} + if err := json.Unmarshal([]byte(m.Payload), &target); err != nil { + return nil, err + } + + switch r.Target { + case "payload": + // Match against only the (deserialized) payload. + case "msg": + // Match against the full message + // (with topic and deserialized + // payload). + target = map[string]interface{}{ + "Topic": m.Topic, + "Payload": target, + } + default: + return nil, Brokenf("bad Recv Target: '%s'", r.Target) + } + + ctx.Inddf(" match target: %s", JSON(target)) + + if r.Schema != "" { + if err := validateSchema(ctx, r.Schema, m.Payload); err != nil { + return nil, err + } + } + + target = Canon(target) + t.Bindings.Clean(ctx, r.ClearBindings) + pattern, err := t.Bindings.Bind(ctx, r.Pattern) + if err != nil { + return nil, err + } + + ctx.Inddf(" bound pattern: %s", JSON(pattern)) + return match.Match(pattern, target, match.NewBindings()) +} + +// extendBindings updates the Test bindings based on the +// match-generated bindings. +// +// This code doesn't actually use the Recv, but it's here as a method +// anyway for organization (?). +func (r *Recv) extendBindings(ctx *Ctx, t *Test, bss []match.Bindings) error { + if 1 < len(bss) { + // Let's protest if we get + // multiple sets of bindings. + // + // Better safe than sorry? If + // we start running into this + // situation, let's figure out + // the best way to proceed. + // Otherwise we might not notice + // unintended behavior. + return fmt.Errorf("multiple bindings sets: %s", JSON(bss)) + } + + // Extend rather than replace + // t.Bindings. Note that we have to + // extend t.Bindings rather than replace + // it due to the bindings substitution + // logic. See the comments above + // 'Match' above. + // + // ToDo: Contemplate possibility for + // inconsistencies. + // + // Thanks, Carlos, for this fix! + if t.Bindings == nil { + // Some unit tests might not + // have initialized t.Bindings. + t.Bindings = make(map[string]interface{}) + } + for p, v := range bss[0] { + if x, have := t.Bindings[p]; have { + // Let's see if we are + // changing an existing + // binding. If so, note + // that. + js0 := JSON(v) + js1 := JSON(x) + if js0 != js1 { + ctx.Indf(" Updating binding for %s", p) + } + } + t.Bindings[p] = v + } + + return nil +} + +// checkGuard invokes the Guard (if any) and returns whether the Guard +// is satisfied. +func (r *Recv) checkGuard(ctx *Ctx, t *Test, m Msg, bss []match.Bindings) (bool, error) { + if r.Guard == "" { + return true, nil + } + + ctx.Indf(" Recv guard") + src, err := t.prepareSource(ctx, r.Guard) + if err != nil { + return false, err + } + + env := t.jsEnv(ctx) + can := Canon(bss) + env["bindingss"] = can + env["bss"] = can + env["msg"] = m + + x, err := JSExec(ctx, src, env) + if f, is := IsFailure(x); is { + return false, f + } + if f, is := IsFailure(err); is { + return false, f + } + if err != nil { + return false, err + } + + switch vv := x.(type) { + case bool: + if !vv { + ctx.Indf(" Recv guard not pleased") + return false, nil + } + ctx.Indf(" Recv guard satisfied") + default: + return false, Brokenf("Guard Javascript returned a %T (%v) and not a bool", x, x) + } + + return true, nil +} + +// runRun runs the Run (if any). +func (r *Recv) runRun(ctx *Ctx, t *Test, m Msg, bss []match.Bindings) error { + if r.Run == "" { + return nil + } + src, err := t.prepareSource(ctx, r.Run) + if err != nil { + return err + } + + env := t.jsEnv(ctx) + can := Canon(&bss) + env["bindingss"] = can + env["bss"] = can + env["msg"] = m + + _, err = JSExec(ctx, src, env) + return err +} + +// verify is the top-level method for verifying an incoming message. +func (r *Recv) verify(ctx *Ctx, t *Test, m Msg) (bool, error) { + var ( + err error + bss []match.Bindings + ) + + if r.Regexp != "" { + if r.Target != "payload" { + return false, Brokenf("can only regexp-match against payload (not also topic)") + } + ctx.Inddf(" regexp: %s", r.Regexp) + bss, err = RegexpMatch(r.Regexp, m.Payload) + } else { + ctx.Inddf(" pattern: %s", JSON(r.Pattern)) + bss, err = r.attemptMatch(ctx, t, m) + } + + if err != nil { + return false, err + } + ctx.Indf(" result: %v", 0 < len(bss)) + ctx.Inddf(" bss: %s", JSON(bss)) + + if 0 == len(bss) { + return false, nil + } + + if err = r.extendBindings(ctx, t, bss); err != nil { + return false, err + } + + happy, err := r.checkGuard(ctx, t, m, bss) + if err != nil { + return false, err + } + if !happy { + return false, nil + } + + ctx.Indf(" Recv satisfied") + ctx.Inddf(" t.Bindings: %s", JSON(t.Bindings)) + + if err = r.runRun(ctx, t, m, bss); err != nil { + return false, err + } + + return true, nil +} + +// Exec performs that main Recv processing. +func (r *Recv) Exec(ctx *Ctx, t *Test) error { + var ( + timeout = r.Timeout + in = r.ch.Recv(ctx) + attempts = 0 + ) + + if timeout == 0 { + timeout = time.Second * 60 * 20 * 24 + } + + tm := time.NewTimer(timeout) + + if r.Regexp != "" { + ctx.Inddf(" Recv regexp %s", r.Regexp) + } else { + ctx.Inddf(" Recv pattern (%T) %v", r.Pattern, r.Pattern) + } + + ctx.Inddf(" Recv target %s", r.Target) + for { + select { + case <-ctx.Done(): + ctx.Indf(" Recv canceled") + return nil + case <-tm.C: + ctx.Indf(" Recv timeout (%v)", timeout) + return fmt.Errorf("timeout after %s waiting for %s", timeout, r.Pattern) + case m := <-in: + + ctx.Indf(" Recv dequeuing topic '%s' (vs '%s')", m.Topic, r.Topic) + ctx.Inddf(" %s", m.Payload) + + // Verify that either no Recv topic was + // provided or that the Recv topic is equal to + // the message topic. Might want to + // generalize. See Issue 105. + if r.Topic == "" || r.Topic == m.Topic { + // Only increment the number of attempts given a topic match. + attempts++ + + ctx.Indf(" Recv match attempt %d:", attempts) + happy, err := r.verify(ctx, t, m) + if err != nil { + return err + } + if happy { + return nil + } + } + + // Give up if the receiver attempts was + // specified (not 0) and that the actual + // number of attempts has been reached + if r.Attempts != 0 && attempts >= r.Attempts { + ctx.Inddf(" attempts: %d of %d", attempts, r.Attempts) + ctx.Inddf(" topic: %s", r.Topic) + + // Make a nice error message. + var match string + if r.Regexp == "" { + match = fmt.Sprintf("pattern: %s", r.Pattern) + } else { + match = fmt.Sprintf("regexp: %s", r.Regexp) + } + var topic string + if r.Topic == "" { + topic = "" + } else { + topic = r.Topic + } + return fmt.Errorf("%d attempt(s) reached; expected maximum of %d attempt(s) to match %s (topic %s)", + attempts, r.Attempts, match, topic) + } + } + } + + // Should never get here. + // + // Let us become thoroughly sensible of the weakness, + // blindness, and narrow limits of human reason. + // + // --David Hume + // + return fmt.Errorf("impossible!") +} diff --git a/dsl/recv_test.go b/dsl/recv_test.go new file mode 100644 index 0000000..fe851e3 --- /dev/null +++ b/dsl/recv_test.go @@ -0,0 +1,283 @@ +package dsl + +import ( + "testing" + + "github.com/Comcast/sheens/match" +) + +func TestRecvSubstitute(t *testing.T) { + ctx, _, tst := newTest(t) + + desired := "tacos" + tst.Bindings = Bindings{ + "?desired": desired, + } + + r0 := &Recv{ + Topic: "{?desired}", + Run: "I like {?desired}.", + // ToDo: A lot more. + } + + r1, err := r0.Substitute(ctx, tst) + if err != nil { + t.Fatal(err) + } + + if r1.Topic != desired { + t.Fatal(r1.Topic) + } + + if r1.Run != "I like "+desired+"." { + t.Fatal(r1.Topic) + } +} + +func TestRecvValidateSchema(t *testing.T) { + ctx := NewCtx(nil) + ctx.LogLevel = "NONE" + schema := "file://../demos/order.json" + t.Run("happy", func(t *testing.T) { + msg := `{"want":"chips"}` + if err := validateSchema(ctx, schema, msg); err != nil { + t.Fatal(err) + } + }) + t.Run("sad", func(t *testing.T) { + msg := `{"need":"queso"}` + if err := validateSchema(ctx, schema, msg); err == nil { + t.Fatal("'want' was required") + } + }) +} + +func TestRecvAttemptMatch(t *testing.T) { + + // ToDo: So much more ... + + ctx, _, tst := newTest(t) + + m := Msg{ + Topic: "questions", + Payload: `"To be or not to be?"`, + } + + r := &Recv{ + Topic: "questions", + Pattern: `"?q"`, + } + + var err error + if r, err = r.Substitute(ctx, tst); err != nil { + t.Fatal(err) + } + + bss, err := r.attemptMatch(ctx, tst, m) + if err != nil { + t.Fatal(err) + } + + if len(bss) != 1 { + t.Fatal(bss) + } + if _, have := bss[0]["?q"]; !have { + t.Fatal(bss[0]) + } +} + +func TestRecvExtendBindings(t *testing.T) { + var ( + ctx, _, tst = newTest(t) + r = &Recv{} + ) + ctx.quiet() + + t.Run("boring", func(t *testing.T) { + bss := []match.Bindings{ + match.Bindings{ + "one": 1, + }, + } + + if err := r.extendBindings(ctx, tst, bss); err != nil { + t.Fatal(nil) + } + }) + + t.Run("extend", func(t *testing.T) { + tst.Bindings = Bindings{ + "zero": 0, + } + + bss := []match.Bindings{ + match.Bindings{ + "one": 1, + }, + } + + if err := r.extendBindings(ctx, tst, bss); err != nil { + t.Fatal(nil) + } + + if len(tst.Bindings) != 2 { + t.Fatal(tst.Bindings) + } + }) + + t.Run("update", func(t *testing.T) { + tst.Bindings = Bindings{ + "like": "kale", + } + + bss := []match.Bindings{ + match.Bindings{ + "like": "queso", + }, + } + + if err := r.extendBindings(ctx, tst, bss); err != nil { + t.Fatal(nil) + } + + if len(tst.Bindings) != 1 { + t.Fatal(tst.Bindings) + } + + x, have := tst.Bindings["like"] + if !have { + t.Fatal(tst.Bindings) + } + if x != "queso" { + t.Fatal(x) + } + }) + + t.Run("multiple", func(t *testing.T) { + bss := []match.Bindings{ + match.Bindings{ + "one": 1, + }, + match.Bindings{ + "two": 2, + }, + } + + if err := r.extendBindings(ctx, tst, bss); err == nil { + t.Fatal(len(bss)) + } + }) + +} + +func TestRecvCheckGuard(t *testing.T) { + + // ToDo: Much more. + var ( + ctx, _, tst = newTest(t) + m = Msg{} + ) + ctx.quiet() + + t.Run("happy", func(t *testing.T) { + var ( + bss = []match.Bindings{ + match.Bindings{ + "likes": "queso", + }, + } + r = &Recv{ + Guard: `return bs["likes"] == "queso";`, + } + ) + + tst.Bindings = Bindings(bss[0]) + happy, err := r.checkGuard(ctx, tst, m, bss) + if err != nil { + t.Fatal(err) + } + if !happy { + t.Fatal("sad") + } + }) + + t.Run("sad", func(t *testing.T) { + var ( + bss = []match.Bindings{ + match.Bindings{ + "likes": "kale", + }, + } + r = &Recv{ + Guard: `return bs["likes"] == "queso";`, + } + ) + + tst.Bindings = Bindings(bss[0]) + happy, err := r.checkGuard(ctx, tst, m, bss) + if err != nil { + t.Fatal(err) + } + if happy { + t.Fatal("wrong") + } + }) +} + +func TestRecvRunRun(t *testing.T) { + + var ( + ctx, _, tst = newTest(t) + m = Msg{} + bss = []match.Bindings{ + match.Bindings{ + "likes": "queso", + }, + } + r = &Recv{ + Run: `test.Bindings["enjoy"] = bs["likes"];`, + } + ) + + tst.Bindings = Bindings(bss[0]) + err := r.runRun(ctx, tst, m, bss) + if err != nil { + t.Fatal(err) + } + x, have := tst.Bindings["enjoy"] + if !have { + t.Fatal(tst.Bindings) + } + if x != "queso" { + t.Fatal(x) + } + +} + +func TestRecvVerify(t *testing.T) { + var ( + ctx, _, tst = newTest(t) + m = Msg{ + Payload: `{"likes":"tacos"}`, + } + r = &Recv{ + Pattern: MaybeParseJSON(`{"likes":"?enjoy"}`), + Guard: `return bs["?enjoy"] == "tacos";`, + } + ) + + ctx.quiet() + + var err error + if r, err = r.Substitute(ctx, tst); err != nil { + t.Fatal(err) + } + + happy, err := r.verify(ctx, tst, m) + if err != nil { + t.Fatal(err) + } + if !happy { + t.Fatal("sad") + } +}