Files
releases/compile/compile_test.go
T
Torin Sandall 91027d1e26 cmd+compile: add new plan target
This commit adds a new "plan" target to the compile package and build
command. This targets lets users compile out query plans into
bundles. This way the compiled query plan can be consumed by
transpilers/interpreters outside of OPA.

Fixes #4133

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
2022-01-29 06:25:30 -08:00

1412 lines
31 KiB
Go

package compile
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path"
"reflect"
"sort"
"strings"
"testing"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/format"
"github.com/open-policy-agent/opa/internal/ref"
"github.com/open-policy-agent/opa/loader"
"github.com/open-policy-agent/opa/util"
"github.com/open-policy-agent/opa/util/test"
)
func TestOrderedStringSet(t *testing.T) {
var ss orderedStringSet
result := ss.Append("a", "b", "b", "a", "e", "c", "e")
if !reflect.DeepEqual(result, orderedStringSet{"a", "b", "e", "c"}) {
t.Fatal(result)
}
}
func TestCompilerInitErrors(t *testing.T) {
ctx := context.Background()
tests := []struct {
note string
c *Compiler
want error
}{
{
note: "bad target",
c: New().WithTarget("deadbeef"),
want: fmt.Errorf("invalid target \"deadbeef\""),
},
{
note: "optimizations require entrypoint",
c: New().WithOptimizationLevel(1),
want: errors.New("bundle optimizations require at least one entrypoint"),
},
{
note: "wasm compilation requires at least one entrypoint",
c: New().WithTarget("wasm"),
want: errors.New("wasm compilation requires at least one entrypoint"),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
err := tc.c.Build(ctx)
if err == nil {
t.Fatal("expected error")
} else if err.Error() != tc.want.Error() {
t.Fatalf("expected %v but got %v", tc.want, err)
}
})
}
}
func TestCompilerLoadError(t *testing.T) {
test.WithTempFS(nil, func(root string) {
err := New().WithPaths(path.Join(root, "does-not-exist")).Build(context.Background())
if err == nil {
t.Fatal("expected failure")
}
})
}
func TestCompilerLoadAsBundleSuccess(t *testing.T) {
ctx := context.Background()
files := map[string]string{
"b1/.manifest": `{"roots": ["b1"]}`,
"b1/test.rego": `
package b1.test
p = 1`,
"b1/data.json": `
{"b1": {"k": "v"}}`,
"b2/.manifest": `{"roots": ["b2"]}`,
"b2/data.json": `
{"b2": {"k2": "v2"}}`,
}
test.WithTempFS(files, func(root string) {
root1 := path.Join(root, "b1")
root2 := path.Join(root, "b2")
compiler := New().
WithPaths(root1, root2).
WithAsBundle(true)
err := compiler.Build(ctx)
if err != nil {
t.Fatal(err)
}
// Verify result is just merger of two bundles.
a, err := loader.NewFileLoader().AsBundle(root1)
if err != nil {
panic(err)
}
b, err := loader.NewFileLoader().AsBundle(root2)
if err != nil {
panic(err)
}
exp, err := bundle.Merge([]*bundle.Bundle{a, b})
if err != nil {
panic(err)
}
err = exp.FormatModules(false)
if err != nil {
t.Fatal(err)
}
if !compiler.bundle.Equal(*exp) {
t.Fatalf("expected %v but got %v", exp, compiler.bundle)
}
expRoots := []string{"b1", "b2"}
expManifest := bundle.Manifest{
Roots: &expRoots,
}
if !compiler.bundle.Manifest.Equal(expManifest) {
t.Fatalf("expected %v but got %v", compiler.bundle.Manifest, expManifest)
}
})
}
func TestCompilerLoadAsBundleMergeError(t *testing.T) {
ctx := context.Background()
// Omit manifests (defaulting to '') to trigger a merge error
files := map[string]string{
"b1/test.rego": `
package b1.test
p = 1`,
"b1/data.json": `
{"b1": {"k": "v"}}`,
"b2/data.json": `
{"b2": {"k2": "v2"}}`,
}
test.WithTempFS(files, func(root string) {
root1 := path.Join(root, "b1")
root2 := path.Join(root, "b2")
compiler := New().
WithPaths(root1, root2).
WithAsBundle(true)
err := compiler.Build(ctx)
if err == nil || err.Error() != "bundle merge failed: manifest has overlapped roots: '' and ''" {
t.Fatal(err)
}
})
}
func TestCompilerLoadFilesystem(t *testing.T) {
files := map[string]string{
"test.rego": `
package b1.test
p = 1`,
"data.json": `
{"b1": {"k": "v"}}`,
}
test.WithTempFS(files, func(root string) {
compiler := New().
WithPaths(root)
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
// Verify result is just bundle load.
exp, err := loader.NewFileLoader().AsBundle(root)
if err != nil {
panic(err)
}
err = exp.FormatModules(false)
if err != nil {
t.Fatal(err)
}
if !compiler.bundle.Equal(*exp) {
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, compiler.bundle)
}
})
}
func TestCompilerLoadHonorsFilter(t *testing.T) {
files := map[string]string{
"test.rego": `
package b1.test
p = 1`,
"data.json": `
{"b1": {"k": "v"}}`,
}
test.WithTempFS(files, func(root string) {
compiler := New().
WithPaths(root).
WithFilter(func(abspath string, _ os.FileInfo, _ int) bool {
return strings.HasSuffix(abspath, ".json")
})
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.Data) > 0 {
t.Fatal("expected no data to be loaded")
}
})
}
func TestCompilerInputBundle(t *testing.T) {
b := &bundle.Bundle{
Modules: []bundle.ModuleFile{
{
URL: "/foo.rego",
Path: "/foo.rego",
Raw: []byte("package test\np = 7"),
Parsed: ast.MustParseModule("package test\np = 7"),
},
},
}
compiler := New().WithBundle(b)
if err := compiler.Build(context.Background()); err != nil {
t.Fatal(err)
}
exp := "package test\n\np = 7\n"
if exp != string(compiler.Bundle().Modules[0].Raw) {
t.Fatalf("expected module to have been formatted (output different than expected):\n\ngot: %v\n\nwant: %v", string(compiler.Bundle().Modules[0].Raw), exp)
}
}
func TestCompilerInputInvalidBundle(t *testing.T) {
b := &bundle.Bundle{
Modules: []bundle.ModuleFile{
{
URL: "/url",
Path: "/foo.rego",
Raw: []byte("package test\np = 0"),
Parsed: ast.MustParseModule("package test\np = 0"),
},
{
URL: "/url",
Path: "/bar.rego",
Raw: []byte("package test\nq = 1"),
Parsed: ast.MustParseModule("package test\nq = 1"),
},
},
}
compiler := New().WithBundle(b)
if err := compiler.Build(context.Background()); err == nil {
t.Fatal("duplicate module URL not detected")
} else if err.Error() != "duplicate module URL: /url" {
t.Fatal(err)
}
}
func TestCompilerError(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
p { p }`,
}
test.WithTempFS(files, func(root string) {
compiler := New().
WithPaths(root)
err := compiler.Build(context.Background())
if err == nil {
t.Fatal("expected error")
}
astErr, ok := err.(ast.Errors)
if !ok || len(astErr) != 1 || astErr[0].Code != ast.RecursionErr {
t.Fatal("unexpected error:", err)
}
})
}
func TestCompilerOptimizationL1(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
p { q }
q { input.x = data.foo }`,
"data.json": `
{"foo": 1}`,
}
test.WithTempFS(files, func(root string) {
compiler := New().
WithPaths(root).
WithOptimizationLevel(1).
WithEntrypoints("test/p")
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
optimizedExp := ast.MustParseModule(`
package test
default p = false
p { data.test.q = X; X }
q { input.x = 1 }
`)
// NOTE(tsandall): PE generates vars with wildcard prefix. Instead of
// constructing the AST manually, just rewrite to the expected value
// here. If this becomes a common pattern, we could refactor (e.g.,
// allow caller to control var prefix, split into a reusable function,
// etc.)
_, err = ast.TransformVars(optimizedExp, func(x ast.Var) (ast.Value, error) {
if x == ast.Var("X") {
return ast.Var("$_term_1_01"), nil
}
return x, nil
})
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.Modules) != 1 {
t.Fatalf("expected 1 module but got: %v", compiler.bundle.Modules)
}
if !compiler.bundle.Modules[0].Parsed.Equal(optimizedExp) {
t.Fatalf("expected optimized module to be:\n\n%v\n\ngot:\n\n%v", optimizedExp, compiler.bundle.Modules[0])
}
})
}
func TestCompilerOptimizationL2(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
p { q }
q { input.x = data.foo }`,
"data.json": `
{"foo": 1}`,
}
test.WithTempFS(files, func(root string) {
compiler := New().
WithPaths(root).
WithOptimizationLevel(2).
WithEntrypoints("test/p")
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
prunedExp := ast.MustParseModule(`
package test
q { input.x = data.foo }`)
optimizedExp := ast.MustParseModule(`
package test
default p = false
p { input.x = 1 }
`)
if len(compiler.bundle.Modules) != 2 {
t.Fatalf("expected two modules but got: %v", compiler.bundle.Modules)
}
if !compiler.bundle.Modules[0].Parsed.Equal(prunedExp) {
t.Fatalf("expected pruned module to be:\n\n%v\n\ngot:\n\n%v", prunedExp, compiler.bundle.Modules[0])
}
if !compiler.bundle.Modules[1].Parsed.Equal(optimizedExp) {
t.Fatalf("expected optimized module to be:\n\n%v\n\ngot:\n\n%v", optimizedExp, compiler.bundle.Modules[1])
}
})
}
// NOTE(sr): we override this to not depend on build tags in tests
func wasmABIVersions(vs ...ast.WasmABIVersion) *ast.Capabilities {
caps := ast.CapabilitiesForThisVersion()
caps.WasmABIVersions = vs
return caps
}
func TestCompilerWasmTarget(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = 7
q = p+1`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q").
WithCapabilities(wasmABIVersions(ast.WasmABIVersion{Version: 1}))
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.WasmModules) == 0 {
t.Fatal("expected to find compiled wasm module")
}
if len(compiler.bundle.Wasm) != 0 {
t.Error("expected NOT to find deprecated bundle `Wasm` value")
}
ensureEntrypointRemoved(t, compiler.bundle, "test/p")
})
}
// If we're building a wasm bundle, and the `opa` binary we use to do that
// does not support wasm _itself_, then it shouldn't bother.
func TestCompilerWasmTargetWithCapabilitiesUnset(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = 7
q = p+1`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q")
err := compiler.Build(context.Background())
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
}
func TestCompilerWasmTargetWithCapabilitiesMismatch(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = 7
q = p+1`,
}
test.WithTempFS(files, func(root string) {
for note, wabis := range map[string][]ast.WasmABIVersion{
"none": {},
"mismatch": {{Version: 0}, {Version: 1, Minor: 2000}},
} {
t.Run(note, func(t *testing.T) {
caps := ast.CapabilitiesForThisVersion()
caps.WasmABIVersions = wabis
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q").
WithCapabilities(caps)
err := compiler.Build(context.Background())
if err == nil {
t.Fatal("expected err, got nil")
}
})
}
})
}
func TestCompilerWasmTargetMultipleEntrypoints(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = true`,
"policy.rego": `package policy
authz = true`,
"mask.rego": `package system.log
mask["/input/password"]`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "policy/authz").
WithCapabilities(wasmABIVersions(ast.WasmABIVersion{Version: 1}))
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.WasmModules) != 1 {
t.Fatalf("expected 1 Wasm modules, got: %d", len(compiler.bundle.WasmModules))
}
expManifest := bundle.Manifest{}
expManifest.Init()
expManifest.WasmResolvers = []bundle.WasmResolver{
{
Entrypoint: "test/p",
Module: "/policy.wasm",
},
{
Entrypoint: "policy/authz",
Module: "/policy.wasm",
},
}
if !compiler.bundle.Manifest.Equal(expManifest) {
t.Fatalf("\nExpected manifest: %+v\nGot: %+v\n", expManifest, compiler.bundle.Manifest)
}
ensureEntrypointRemoved(t, compiler.bundle, "test/p")
ensureEntrypointRemoved(t, compiler.bundle, "policy/authz")
})
}
func TestCompilerWasmTargetEntrypointDependents(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p { q }
q { r }
r = 1
s = 2
z { r }`}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/r", "test/z").
WithCapabilities(wasmABIVersions(ast.WasmABIVersion{Version: 1}))
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.WasmModules) != 1 {
t.Fatalf("expected 1 Wasm modules, got: %d", len(compiler.bundle.WasmModules))
}
expManifest := bundle.Manifest{}
expManifest.Init()
expManifest.WasmResolvers = []bundle.WasmResolver{
{
Entrypoint: "test/r",
Module: "/policy.wasm",
},
{
Entrypoint: "test/z",
Module: "/policy.wasm",
},
{
Entrypoint: "test/p",
Module: "/policy.wasm",
},
{
Entrypoint: "test/q",
Module: "/policy.wasm",
},
}
if !compiler.bundle.Manifest.Equal(expManifest) {
t.Fatalf("\nExpected manifest: %+v\nGot: %+v\n", expManifest, compiler.bundle.Manifest)
}
ensureEntrypointRemoved(t, compiler.bundle, "test/p")
ensureEntrypointRemoved(t, compiler.bundle, "test/q")
ensureEntrypointRemoved(t, compiler.bundle, "test/r")
})
}
func TestCompilerWasmTargetLazyCompile(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p { input.x = q }
q = "foo"`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p").WithOptimizationLevel(1).
WithCapabilities(wasmABIVersions(ast.WasmABIVersion{Version: 1}))
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.WasmModules) == 0 {
t.Fatal("expected to find compiled wasm module")
}
if _, exists := compiler.compiler.Modules["optimized/test.rego"]; !exists {
t.Fatal("expected to find optimized module on compiler")
}
ensureEntrypointRemoved(t, compiler.bundle, "test/p")
})
}
func ensureEntrypointRemoved(t *testing.T, b *bundle.Bundle, e string) {
t.Helper()
r, err := ref.ParseDataPath(e)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
for _, mf := range b.Modules {
for _, rule := range mf.Parsed.Rules {
if rule.Path().Equal(r) {
t.Errorf("expected entrypoint to be removed from rego all modules in bundle, found rule: %s in %s", rule.Path(), mf.Path)
}
}
}
}
func TestCompilerPlanTarget(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = 7
q = p+1`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithTarget("plan").WithEntrypoints("test/p", "test/q")
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if len(compiler.bundle.PlanModules) == 0 {
t.Fatal("expected to find compiled plan module")
}
})
}
func TestCompilerSetRevision(t *testing.T) {
files := map[string]string{
"test.rego": `package test
p = true`,
}
test.WithTempFS(files, func(root string) {
compiler := New().WithPaths(root).WithRevision("deadbeef")
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
if compiler.bundle.Manifest.Revision != "deadbeef" {
t.Fatal("expected revision to be set but got:", compiler.bundle.Manifest)
}
})
}
func TestCompilerOutput(t *testing.T) {
// NOTE(tsandall): must use format package here because the compiler formats.
files := map[string]string{
"test.rego": string(format.MustAst(ast.MustParseModule(`package test
p { input.x = data.foo }`))),
"data.json": `{"foo": 1}`,
}
test.WithTempFS(files, func(root string) {
buf := bytes.NewBuffer(nil)
compiler := New().WithPaths(root).WithOutput(buf)
err := compiler.Build(context.Background())
if err != nil {
t.Fatal(err)
}
// Check that the written bundle is expected.
result, err := bundle.NewReader(buf).Read()
if err != nil {
t.Fatal(err)
}
exp, err := loader.NewFileLoader().AsBundle(root)
if err != nil {
t.Fatal(err)
}
if !exp.Equal(result) {
t.Fatalf("expected:\n\n%+v\n\ngot:\n\n%+v", *exp, result)
}
if !exp.Manifest.Equal(result.Manifest) {
t.Fatalf("expected:\n\n%+v\n\ngot:\n\n%+v", exp.Manifest, result.Manifest)
}
// Check that the returned bundle is the expected.
compiled := compiler.Bundle()
if !exp.Equal(*compiled) {
t.Fatalf("expected:\n\n%v\n\ngot:\n\n%v", *exp, *compiled)
}
if !exp.Manifest.Equal(compiled.Manifest) {
t.Fatalf("expected:\n\n%v\n\ngot:\n\n%v", exp.Manifest, compiled.Manifest)
}
})
}
func TestOptimizerNoops(t *testing.T) {
tests := []struct {
note string
entrypoints []string
modules map[string]string
}{
{
note: "recursive result",
entrypoints: []string{"data.test.foo"},
modules: map[string]string{
"test.rego": `
package test.foo.bar
p { input.x = 1 }
`,
},
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
o := getOptimizer(tc.modules, "", tc.entrypoints, nil)
cpy := o.bundle.Copy()
err := o.Do(context.Background())
if err != nil {
t.Fatal(err)
}
if !o.bundle.Equal(cpy) {
t.Fatalf("Expected no change:\n\n%v\n\nGot:\n\n%v", prettyBundle{cpy}, prettyBundle{*o.bundle})
}
})
}
}
func TestOptimizerErrors(t *testing.T) {
tests := []struct {
note string
entrypoints []string
modules map[string]string
wantErr error
}{
{
note: "undefined entrypoint",
entrypoints: []string{"data.test.p"},
wantErr: fmt.Errorf("undefined entrypoint data.test.p"),
},
{
note: "compile error",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p { data.test.p }
`,
},
wantErr: fmt.Errorf("1 error occurred: test.rego:3: rego_recursion_error: rule p is recursive: p -> p"),
},
{
note: "partial eval error",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p { {k: v | k = ["a", "a"][_]; v = [0, 1][_] } }
`,
},
wantErr: fmt.Errorf("test.rego:3: eval_conflict_error: object keys must be unique"),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
o := getOptimizer(tc.modules, "", tc.entrypoints, nil)
cpy := o.bundle.Copy()
got := o.Do(context.Background())
if got == nil || got.Error() != tc.wantErr.Error() {
t.Fatalf("expected error to be %v but got %v", tc.wantErr, got)
}
if !o.bundle.Equal(cpy) {
t.Fatalf("Expected no change:\n\n%v\n\nGot:\n\n%v", prettyBundle{cpy}, prettyBundle{*o.bundle})
}
})
}
}
func TestOptimizerOutput(t *testing.T) {
tests := []struct {
note string
entrypoints []string
modules map[string]string
data string
roots []string
wantModules map[string]string
}{
{
note: "rule pruning",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p {
q[input.x]
}
q[1]
q[2]
q[3]
`,
},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p = __result__ { 1 = input.x; __result__ = true }
p = __result__ { 2 = input.x; __result__ = true }
p = __result__ { 3 = input.x; __result__ = true }
`,
"test.rego": `
package test
q[1]
q[2]
q[3]
`,
},
},
{
note: "support rules",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
default p = false
p { q[input.x] }
q[1]
q[2]`,
},
wantModules: map[string]string{
"optimized/test.rego": `
package test
default p = false
p = true { 1 = input.x }
p = true { 2 = input.x }
`,
"test.rego": `
package test
q[1]
q[2]
`,
},
},
{
note: "multiple entrypoints",
entrypoints: []string{"data.test.p", "data.test.r", "data.test.s"},
modules: map[string]string{
"test.rego": `
package test
p {
q[input.x]
}
r {
q[input.x]
}
s {
q[input.x]
}
q[1]
`,
},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p = __result__ { 1 = input.x; __result__ = true }
`,
"optimized/test.1.rego": `
package test
r = __result__ { 1 = input.x; __result__ = true }
`,
"optimized/test.2.rego": `
package test
s = __result__ { 1 = input.x; __result__ = true }
`,
"test.rego": `
package test
q[1] { true }
`,
},
},
{
note: "package pruning",
entrypoints: []string{"data.test.foo"},
modules: map[string]string{
"test.rego": `
package test.foo.bar
p = true
`,
},
wantModules: map[string]string{
"optimized/test.rego": `
package test
foo = __result__ { __result__ = {"bar": {"p": true}} }`,
},
},
{
note: "entrypoint dependent integrity",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p { q[input.x] }
q[x] {
s[x]
}
s[1]
s[2]
t {
p
}
r { t with q as {3} }
`,
},
wantModules: map[string]string{
"optimized/test.1.rego": `
package test
p = __result__ { data.test.q[input.x]; __result__ = true }
`,
"optimized/test.rego": `
package test
q[1] { true }
q[2] { true }
`,
"test.rego": `
package test
s[1] { true }
s[2] { true }
t { p }
r = true { t with q as {3} }
`,
},
},
{
note: "output filename safety",
entrypoints: []string{`data.test["foo bar"].p`},
modules: map[string]string{
"x.rego": `
package test["foo bar"] # package does not match safe pattern so use alt. format
p { q[input.x] }
q[1]
q[2]
`,
},
wantModules: map[string]string{
"optimized/partial/0/0.rego": `
package test["foo bar"]
p = __result__ { 1 = input.x; __result__ = true }
p = __result__ { 2 = input.x; __result__ = true }
`,
"x.rego": `
package test["foo bar"]
q[1]
q[2]
`,
},
},
{
note: "generated package namespace",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p { not q }
q { k[input.a]; k[input.b] } # generate a product that is not inlined
k = {1,2,3}
`,
},
wantModules: map[string]string{
"optimized/partial.rego": `
package partial
__not1_0_2__ = true { 1 = input.a; 1 = input.b }
__not1_0_2__ = true { 1 = input.a; 2 = input.b }
__not1_0_2__ = true { 1 = input.a; 3 = input.b }
__not1_0_2__ = true { 2 = input.a; 1 = input.b }
__not1_0_2__ = true { 2 = input.a; 2 = input.b }
__not1_0_2__ = true { 2 = input.a; 3 = input.b }
__not1_0_2__ = true { 3 = input.a; 1 = input.b }
__not1_0_2__ = true { 3 = input.a; 2 = input.b }
__not1_0_2__ = true { 3 = input.a; 3 = input.b }
`,
"optimized/test.rego": `
package test
p = __result__ { not data.partial.__not1_0_2__; __result__ = true }
`,
"test.rego": `
package test
q = true { k[input.a]; k[input.b] }
k = {1, 2, 3} { true }
`,
},
},
{
note: "infer unknowns from roots",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p {
q[x]
data.external.users[x] == input.user
}
q["foo"]
q["bar"]
`,
},
roots: []string{"test"},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p = __result__ { data.external.users.bar = input.user; __result__ = true }
p = __result__ { data.external.users.foo = input.user; __result__ = true }
`,
"test.rego": `
package test
q["foo"]
q["bar"]
`,
},
},
{
note: "generate rules with type violations: complete doc",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p {
x := split(input.a, ":")
f(x[0])
}
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
roots: []string{"test"},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p = __result__ { split(input.a, ":", __local3__1); startswith(__local3__1[0], "foo"); __result__ = true }
`,
"test.rego": `
package test
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
},
{
note: "generate rules with type violations: partial set",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p[msg] {
x := split(input.a, ":")
f(x[0])
msg := "test string"
}
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
roots: []string{"test"},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p["test string"] { split(input.a, ":", __local4__1); startswith(__local4__1[0], "foo") }
`,
"test.rego": `
package test
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
},
{
note: "generate rules with type violations: partial object",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p[k] = value {
x := split(input.a, ":")
f(x[0])
k := "a"
value := 1
}
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
roots: []string{"test"},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p["a"] = 1 { split(input.a, ":", __local5__1); startswith(__local5__1[0], "foo") }
`,
"test.rego": `
package test
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
},
{
note: "generate rules with type violations: negation",
entrypoints: []string{"data.test.p"},
modules: map[string]string{
"test.rego": `
package test
p { not q }
q {
x := split(input.a, ":")
f(x[0])
}
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
},
roots: []string{"test"},
wantModules: map[string]string{
"optimized/test.rego": `
package test
p = __result__ { not data.partial.__not1_0_2__; __result__ = true }
`,
"test.rego": `
package test
q = true { assign(x, split(input.a, ":")); f(x[0]) }
f(x) { x == null }
f(x) { startswith(x, "foo") }
`,
"optimized/partial.rego": `
package partial
__not1_0_2__ = true { split(input.a, ":", __local3__3); startswith(__local3__3[0], "foo") }
`,
},
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
o := getOptimizer(tc.modules, tc.data, tc.entrypoints, tc.roots)
original := o.bundle.Copy()
err := o.Do(context.Background())
if err != nil {
t.Fatal(err)
}
exp := &bundle.Bundle{
Modules: getModuleFiles(tc.wantModules, false),
Data: original.Data, // data is not pruned at all today
}
if len(tc.roots) > 0 {
exp.Manifest.Roots = &tc.roots
exp.Manifest.AddRoot("partial") // optimizer will add this automatically
}
exp.Manifest.Revision = "" // optimizations must reset the revision.
if !exp.Equal(*o.bundle) {
t.Errorf("Expected:\n\n%v\n\nGot:\n\n%v", prettyBundle{*exp}, prettyBundle{*o.bundle})
}
if !o.bundle.Manifest.Equal(exp.Manifest) {
t.Errorf("Expected manifest: %v\n\nGot manifest: %v", exp.Manifest, o.bundle.Manifest)
}
})
}
}
func TestRefSet(t *testing.T) {
rs := newRefSet(ast.MustParseRef("input"), ast.MustParseRef("data.foo.bar"))
expFound := []string{
"input",
"input.foo",
"data.foo.bar",
"data.foo.bar.baz",
"data.foo.bar[1]",
}
for _, exp := range expFound {
if !rs.ContainsPrefix(ast.MustParseRef(exp)) {
t.Fatal("expected to find:", exp)
}
}
expNotFound := []string{
"x.bar",
"data",
"data.bar",
"data.foo",
}
for _, exp := range expNotFound {
if rs.ContainsPrefix(ast.MustParseRef(exp)) {
t.Fatal("expected not to find:", exp)
}
}
rs.AddPrefix(ast.MustParseRef("data.foo"))
if !rs.ContainsPrefix(ast.MustParseRef("data.foo")) {
t.Fatal("expected to find data.foo after adding to set")
}
sorted := rs.Sorted()
if len(sorted) != 2 || !sorted[0].Equal(ast.MustParseTerm("data.foo")) || !sorted[1].Equal(ast.MustParseTerm("input")) {
t.Fatal("expected 2 prefixes (data.foo and input) but got:", sorted)
}
// The prefixes should not be affected (because data.foo already exists).
rs.AddPrefix(ast.MustParseRef("data.foo.qux"))
sorted = rs.Sorted()
if len(sorted) != 2 || !sorted[0].Equal(ast.MustParseTerm("data.foo")) || !sorted[1].Equal(ast.MustParseTerm("input")) {
t.Fatal("expected 2 prefixes (data.foo and input) but got:", sorted)
}
}
func getOptimizer(modules map[string]string, data string, entries []string, roots []string) *optimizer {
b := &bundle.Bundle{
Modules: getModuleFiles(modules, true),
}
if data != "" {
b.Data = util.MustUnmarshalJSON([]byte(data)).(map[string]interface{})
}
if len(roots) > 0 {
b.Manifest.Roots = &roots
}
b.Manifest.Init()
b.Manifest.Revision = "DEADBEEF" // ensures that the manifest revision is getting reset
entrypoints := make([]*ast.Term, len(entries))
for i := range entrypoints {
entrypoints[i] = ast.MustParseTerm(entries[i])
}
o := newOptimizer(ast.CapabilitiesForThisVersion(), b).
WithEntrypoints(entrypoints)
o.resultsymprefix = ""
return o
}
func getModuleFiles(src map[string]string, includeRaw bool) []bundle.ModuleFile {
var keys []string
for k := range src {
keys = append(keys, k)
}
sort.Strings(keys)
var modules []bundle.ModuleFile
for _, k := range keys {
module, err := ast.ParseModule(k, src[k])
if err != nil {
panic(err)
}
modules = append(modules, bundle.ModuleFile{
Parsed: module,
Path: k,
URL: k,
})
if includeRaw {
modules[len(modules)-1].Raw = []byte(src[k])
}
}
return modules
}
type prettyBundle struct {
bundle.Bundle
}
func (p prettyBundle) String() string {
buf := []string{fmt.Sprintf("%d module(s) (hiding data):", len(p.Modules)), ""}
for _, mf := range p.Modules {
buf = append(buf, "#")
buf = append(buf, fmt.Sprintf("# Module: %q", mf.Path))
buf = append(buf, "#")
buf = append(buf, mf.Parsed.String())
buf = append(buf, "")
}
return strings.Join(buf, "\n")
}