Files
releases/cmd/test_test.go
T
Stephan Renatus 2f6bf3f12b ast+topdown: exit-early for (constant) complete virtual docs and functions (#3898)
If the following conditions hold for a set of rules returned by the indexer,
it will set EarlyExit: true, and change how the complete virtual doc or
function is evaluated:

- all rule head values are ground
- all rule head values match

This implies that some cases where early exit would be possible will not be
covered:

    p = x {
      x := true
      input.foo == "bar"
    }

    p = x {
      x := true
      input.baz == "quz"
    }

To indicate that "early exit" is possible, the indexer result message is
amended. Also, the "Exit" trace event will have a message of "early" when
"early exit" actually happens in eval:

    $ echo '{"x":"x", "y":"y"}' | opa eval -I -fpretty --explain=full -d r.rego data.r.r
    query:1       Enter data.r.r = _
    query:1       | Eval data.r.r = _
    query:1       | Index data.r.r (matched 2 rules, early exit)
    r.rego:11     | Enter data.r.r
    r.rego:12     | | Eval input.y = "y"
    r.rego:11     | | Exit data.r.r
    query:1       | Exit data.r.r = _
    query:1       Redo data.r.r = _
    query:1       | Redo data.r.r = _
    r.rego:11     | Redo data.r.r
    r.rego:12     | | Redo input.y = "y"
    r.rego:11     | Exit data.r.r early

With `r.rego` as

    package r
    r {
      input.x = "x"
    }
    r = 2 {
      input.z = "z"
    }
    r {
      input.y = "y"
    }

This is done in in a way such that early-exit will abort array/set/object
iterations on data:

    r {
      data.i[_] = "one"
      data.j[_] = "four"
    }

    f(x, y) {
      data.i[_] = x
      data.j[_] = y
    }

Complete rules (r) and functions (f) that iterate over sets, arrays, and
objects from either data (evalTree) or a term that's returned by some
other rule etc (evalTerm).

The CLI and golang packages expose ways to disable 'early-exit':

This is in line with how indexing can be disabled. It's supposed to be
used as a debugging measure, so it's only exposed as a CLI flag to
`opa eval`.

Co-authored-by: Torin Sandall <torinsandall@gmail.com>
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
2021-11-18 10:49:09 +01:00

201 lines
4.6 KiB
Go

package cmd
import (
"bytes"
"context"
"testing"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/topdown"
)
func TestFilterTraceDefault(t *testing.T) {
p := newTestCommandParams()
p.verbose = false
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, p, expected)
}
func TestFilterTraceVerbose(t *testing.T) {
p := newTestCommandParams()
p.verbose = true
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, p, expected)
}
func TestFilterTraceExplainFails(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeFails)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, p, expected)
}
func TestFilterTraceExplainNotes(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeNotes)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
`
verifyFilteredTrace(t, p, expected)
}
func TestFilterTraceExplainFull(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeFull)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Eval data.testing.test_p = _
| Index data.testing.test_p (matched 1 rule, early exit)
| Enter data.testing.test_p
| | Eval data.testing.p with data.x as "bar"
| | Index data.testing.p (matched 1 rule, early exit)
| | Enter data.testing.p
| | | Eval data.testing.x
| | | Index data.testing.x (matched 1 rule, early exit)
| | | Enter data.testing.x
| | | | Eval data.testing.y
| | | | Index data.testing.y (matched 1 rule, early exit)
| | | | Enter data.testing.y
| | | | | Eval true
| | | | | Exit data.testing.y early
| | | | Exit data.testing.x early
| | | Eval trace("test test")
| | | Note "test test"
| | | Eval data.testing.q.foo
| | | Index data.testing.q (matched 1 rule)
| | | Enter data.testing.q
| | | | Eval trace("got this far")
| | | | Note "got this far"
| | | | Eval data.testing.r[x]
| | | | Index data.testing.r (matched 1 rule)
| | | | Enter data.testing.r
| | | | | Eval trace("got this far2")
| | | | | Note "got this far2"
| | | | | Eval x = data.x
| | | | | Fail x = data.x
| | | | | Redo trace("got this far2")
| | | | Fail data.testing.r[x]
| | | | Redo trace("got this far")
| | | Fail data.testing.q.foo
| | | Redo trace("test test")
| | | Redo data.testing.x
| | | Redo data.testing.x
| | | | Redo data.testing.y
| | | | | Redo true
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, p, expected)
}
func verifyFilteredTrace(t *testing.T, params *testCommandParams, expected string) {
filtered := filterTrace(params, failTrace(t))
var buff bytes.Buffer
topdown.PrettyTrace(&buff, filtered)
actual := buff.String()
if actual != expected {
t.Fatalf("Expected:\n\n%s\n\nGot:\n\n%s\n\n", expected, actual)
}
}
func failTrace(t *testing.T) []*topdown.Event {
t.Helper()
mod := `
package testing
p {
x # Always true
trace("test test")
q["foo"]
}
x {
y
}
y {
true
}
q[x] {
some x
trace("got this far")
r[x]
trace("got this far1")
}
r[x] {
trace("got this far2")
x := data.x
}
test_p {
p with data.x as "bar"
}
`
tracer := topdown.NewBufferTracer()
_, err := rego.New(
rego.Module("test.rego", mod),
rego.Trace(true),
rego.QueryTracer(tracer),
rego.Query("data.testing.test_p"),
).Eval(context.Background())
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
return *tracer
}