Files
releases/v1/tester/runner.go
T
Anders Eknert 8f7a0d8495 Add method to match tests by ref prefixes (#8550)
Labeled as experimental as we'll want some time to adjust this
based on real editor integrations. Initial testing looks very
good though, and dramatically reduces the time taken to test
e.g. a package compared to the whole workspace and filtering
the result afterwards.

Fixes #6696

Signed-off-by: Anders Eknert <anders.eknert@apple.com>
2026-04-22 16:25:26 +00:00

1340 lines
37 KiB
Go

// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package tester contains utilities for executing Rego tests.
package tester
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"testing"
"time"
wasm_errors "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/errors"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/loader"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/storage/inmem"
"github.com/open-policy-agent/opa/v1/topdown"
"github.com/open-policy-agent/opa/v1/util"
)
// TestPrefix declares the prefix for all test rules.
const TestPrefix = "test_"
// SkipTestPrefix declares the prefix for tests that should be skipped.
const SkipTestPrefix = "todo_test_"
// Run executes all test cases found under files in path.
func Run(ctx context.Context, paths ...string) ([]*Result, error) {
return RunWithFilter(ctx, nil, paths...)
}
// RunWithFilter executes all test cases found under files in path. The filter
// will be applied to exclude files that should not be included.
func RunWithFilter(ctx context.Context, _ loader.Filter, paths ...string) ([]*Result, error) {
modules, store, err := Load(paths, nil)
if err != nil {
return nil, err
}
ch, err := NewRunner().SetStore(store).Run(ctx, modules)
if err != nil {
return nil, err
}
result := []*Result{}
for r := range ch {
result = append(result, r)
}
return result, nil
}
type SubResult struct {
Name string `json:"name,omitempty"`
Fail bool `json:"fail,omitempty"`
Trace []*topdown.Event `json:"-"`
SubResults SubResultMap `json:"sub_results,omitempty"`
}
type SubResultMap map[string]*SubResult
func (srm SubResultMap) Update(path ast.Array, trace []*topdown.Event) bool {
strPath := make([]string, path.Len())
for i := range path.Len() {
strPath[i] = termToString(path.Elem(i))
}
return srm.update(strPath, 0, trace)
}
func (srm SubResultMap) update(path []string, i int, trace []*topdown.Event) bool {
if i >= len(path) {
return true
}
k := path[i]
entry, ok := srm[k]
if !ok {
entry = &SubResult{
Name: path[i],
Fail: true,
SubResults: SubResultMap{},
}
srm[k] = entry
}
if i == len(path)-1 {
entry.Trace = trace
return entry.Fail
}
fail := entry.SubResults.update(path, i+1, trace)
if fail {
entry.Fail = true
}
return fail
}
type unknownResolver struct{}
func (unknownResolver) Resolve(_ ast.Ref) (any, error) {
return "UNKNOWN", nil
}
type prefixMatchers []ast.Ref
// Match returns true if any of it's prefixes matches ref, or it contains no prefixes.
func (pm prefixMatchers) Match(ref ast.Ref) bool {
return len(pm) == 0 || slices.ContainsFunc(pm, ref.HasPrefix)
}
// AnyPrefixMatcher returns true if prefix is a prefix of any of the matchers, or it contains no prefixes.
func (pm prefixMatchers) AnyPrefixMatcher(prefix ast.Ref) bool {
return len(pm) == 0 || slices.ContainsFunc(pm, func(matcher ast.Ref) bool {
return matcher.HasPrefix(prefix)
})
}
func termToString(t *ast.Term) string {
ti, err := ast.ValueToInterface(t.Value, unknownResolver{})
if err != nil {
return "INVALID"
}
var str string
var ok bool
if str, ok = ti.(string); !ok {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(ti); err != nil {
return "INVALID"
}
str = strings.TrimSpace(buf.String())
}
return str
}
// Result represents a single test case result.
type Result struct {
Location *ast.Location `json:"location"`
Package string `json:"package"`
Name string `json:"name"`
Fail bool `json:"fail,omitempty"`
Error error `json:"error,omitempty"`
Skip bool `json:"skip,omitempty"`
Duration time.Duration `json:"duration"`
Trace []*topdown.Event `json:"trace,omitempty"`
Output []byte `json:"output,omitempty"`
FailedAt *ast.Expr `json:"failed_at,omitempty"`
BenchmarkResult *testing.BenchmarkResult `json:"benchmark_result,omitempty"`
SubResults SubResultMap `json:"sub_results,omitempty"`
}
func newResult(loc *ast.Location, pkg, name string, duration time.Duration, trace []*topdown.Event, output []byte) *Result {
return &Result{
Location: loc,
Package: pkg,
Name: name,
Duration: duration,
Trace: trace,
Output: output,
SubResults: SubResultMap{},
}
}
// Pass returns true if the test case passed.
func (r *Result) Pass() bool {
return !r.Fail && !r.Skip && r.Error == nil
}
func (r *Result) String() string {
return r.string(true)
}
func (r *Result) string(subResults bool) string {
var buf bytes.Buffer
buf.WriteString(r.Package)
buf.WriteByte('.')
buf.WriteString(r.Name)
buf.WriteString(": ")
buf.WriteString(r.outcome())
if r.Skip {
return buf.String()
}
buf.WriteString(" (")
buf.WriteString(r.Duration.String())
buf.WriteByte(')')
if subResults {
buf.WriteByte('\n')
buf.WriteString(r.SubResults.String())
}
return buf.String()
}
func (r *Result) outcome() string {
if r.Pass() {
return "PASS"
}
if r.Fail {
return "FAIL"
}
if r.Skip {
return "SKIPPED"
}
return "ERROR"
}
func (sr *SubResult) String() string {
return sr.Name + ": " + sr.outcome()
}
func (sr *SubResult) outcome() string {
if sr.Fail {
return "FAIL"
}
return "PASS"
}
// Iter is a depth-first iterator over all sub-results.
func (srm SubResultMap) Iter(yield func([]string, *SubResult) bool) {
srm.iter(nil, yield)
}
func (srm SubResultMap) iter(namePrefix []string, yield func([]string, *SubResult) bool) {
for _, k := range util.KeysSorted(srm) {
sr := srm[k]
fullName := make([]string, len(namePrefix)+1)
copy(fullName, namePrefix)
fullName[len(fullName)-1] = k
if !yield(fullName, sr) {
return
}
sr.SubResults.iter(fullName, yield)
}
}
func (srm SubResultMap) String() string {
return srm.string(" ")
}
func (srm SubResultMap) string(indent string) string {
var buf bytes.Buffer
for fullName, sr := range srm.Iter {
buf.WriteString(strings.Repeat(indent, len(fullName)-1))
buf.WriteString(sr.String())
buf.WriteByte('\n')
}
return buf.String()
}
// BenchmarkOptions defines options specific to benchmarking tests
type BenchmarkOptions struct {
ReportAllocations bool
}
// Runner implements simple test discovery and execution.
type Runner struct {
compiler *ast.Compiler
store storage.Store
cover topdown.QueryTracer
trace bool
enablePrintStatements bool
raiseBuiltinErrors bool
runtime *ast.Term
timeout time.Duration
modules map[string]*ast.Module
bundles map[string]*bundle.Bundle
prefixMatchers prefixMatchers
filter string
target string // target type (wasm, rego, etc.)
customBuiltins []*Builtin
defaultRegoVersion ast.RegoVersion
parallel int
}
// NewRunner returns a new runner.
func NewRunner() *Runner {
return &Runner{
timeout: 5 * time.Second,
defaultRegoVersion: ast.DefaultRegoVersion,
parallel: runtime.NumCPU(),
}
}
// SetParallel sets the number of tests that can run in parallel
func (r *Runner) SetParallel(parallel int) *Runner {
if parallel < 1 {
parallel = runtime.NumCPU()
}
r.parallel = parallel
return r
}
// SetDefaultRegoVersion sets the default Rego version to use when compiling modules.
// Not applicable if a custom [ast.Compiler] is set via [SetCompiler].
func (r *Runner) SetDefaultRegoVersion(v ast.RegoVersion) *Runner {
r.defaultRegoVersion = v
return r
}
// SetCompiler sets the compiler used by the runner.
func (r *Runner) SetCompiler(compiler *ast.Compiler) *Runner {
r.compiler = compiler
return r
}
// RaiseBuiltinErrors sets the runner to raise errors encountered by builtins
// such as parsing input.
func (r *Runner) RaiseBuiltinErrors(enabled bool) *Runner {
r.raiseBuiltinErrors = enabled
return r
}
type Builtin struct {
Decl *ast.Builtin
Func func(*rego.Rego)
}
func (r *Runner) AddCustomBuiltins(builtinsList []*Builtin) *Runner {
r.customBuiltins = builtinsList
return r
}
// SetStore sets the store to execute tests over.
func (r *Runner) SetStore(store storage.Store) *Runner {
r.store = store
return r
}
// SetCoverageTracer sets the tracer to use to compute coverage.
//
// Deprecated: Use SetCoverageQueryTracer instead.
func (r *Runner) SetCoverageTracer(tracer topdown.Tracer) *Runner {
if tracer == nil {
return r
}
if qt, ok := tracer.(topdown.QueryTracer); ok {
r.cover = qt
} else {
r.cover = topdown.WrapLegacyTracer(tracer)
}
return r
}
// SetCoverageQueryTracer sets the tracer to use to compute coverage.
func (r *Runner) SetCoverageQueryTracer(tracer topdown.QueryTracer) *Runner {
if tracer == nil {
return r
}
r.cover = tracer
return r
}
// CapturePrintOutput captures print() call outputs during evaluation and
// includes the output in test results.
func (r *Runner) CapturePrintOutput(yes bool) *Runner {
r.enablePrintStatements = yes
return r
}
// EnableTracing enables tracing of evaluation and includes traces in results.
// Tracing is currently mutually exclusive with coverage.
func (r *Runner) EnableTracing(yes bool) *Runner {
r.trace = yes
if r.trace {
r.cover = nil
}
return r
}
// SetRuntime sets runtime information to expose to the evaluation engine.
func (r *Runner) SetRuntime(term *ast.Term) *Runner {
r.runtime = term
return r
}
// SetTimeout sets the timeout for the individual test cases
func (r *Runner) SetTimeout(timout time.Duration) *Runner {
r.timeout = timout
return r
}
// SetModules will add modules to the Runner which will be compiled then used
// for discovering and evaluating tests.
func (r *Runner) SetModules(modules map[string]*ast.Module) *Runner {
r.modules = modules
return r
}
// SetBundles will add bundles to the Runner which will be compiled then used
// for discovering and evaluating tests.
func (r *Runner) SetBundles(bundles map[string]*bundle.Bundle) *Runner {
r.bundles = bundles
return r
}
// SetPrefixMatchers will have the test runner use the provided ref prefixes as a
// filter for which test cases to run- This could either be a shorther path to e.g.
// run all tests in one or more packages, or a full refs pointing to to individual
// tests. Potential matches are evaluated in ascending order based on ref length.
//
// Experimental: This function is used to allow editor integrations more
// flexibility in selecting which tests to run, and the implementation may
// change depending on what we learn from that.
func (r *Runner) SetPrefixMatchers(prefixes ...ast.Ref) *Runner {
r.prefixMatchers = slices.SortedFunc(slices.Values(prefixes), util.SliceLenCompare)
return r
}
// Filter will set a test name regex filter for the test runner. Only test
// cases which match the filter will be run.
func (r *Runner) Filter(regex string) *Runner {
r.filter = regex
return r
}
// Target sets the output target type to use.
func (r *Runner) Target(target string) *Runner {
r.target = target
return r
}
// Run executes all tests contained in supplied modules.
//
// Deprecated: Use RunTests and the Runner#SetModules or Runner#SetBundles
// helpers instead. This will NOT use the modules or bundles set on the Runner.
func (r *Runner) Run(ctx context.Context, modules map[string]*ast.Module) (chan *Result, error) {
return r.SetModules(modules).RunTests(ctx, nil)
}
// RunTests executes tests found in either modules or bundles loaded on the runner.
// Test results are sent as they complete and may arrive in any order.
func (r *Runner) RunTests(ctx context.Context, txn storage.Transaction) (chan *Result, error) {
return r.runTests(ctx, txn, true, r.runTest, r.parallel)
}
// RunBenchmarks executes tests similar to tester.Runner#RunTests but will repeat
// a number of times to get stable performance metrics.
// Each benchmark test is run sequentially.
func (r *Runner) RunBenchmarks(ctx context.Context, txn storage.Transaction, options BenchmarkOptions) (chan *Result, error) {
return r.runTests(ctx, txn, false, func(ctx context.Context, txn storage.Transaction, module *ast.Module, rule *ast.Rule) (result *Result, b bool) {
return r.runBenchmark(ctx, txn, module, rule, options)
}, 1)
}
type run func(context.Context, storage.Transaction, *ast.Module, *ast.Rule) (*Result, bool)
func (r *Runner) setupTestRun(ctx context.Context, txn storage.Transaction, enablePrintStatements bool) (*regexp.Regexp, error) {
var testRegex *regexp.Regexp
var err error
if r.filter != "" {
testRegex, err = regexp.Compile(r.filter)
if err != nil {
return nil, err
}
}
if r.compiler == nil {
capabilities := ast.CapabilitiesForThisVersion()
// Add custom builtins declarations to compiler
for _, builtin := range r.customBuiltins {
capabilities.Builtins = append(capabilities.Builtins, builtin.Decl)
}
r.compiler = ast.NewCompiler().
WithCapabilities(capabilities).
WithEnablePrintStatements(enablePrintStatements).
WithDefaultRegoVersion(r.defaultRegoVersion)
}
// rewrite duplicate test_* rule names as we compile modules
r.compiler.WithStageAfterID(ast.StageRewriteRuleHeadRefs, ast.CompilerStageDefinition{
Name: "RewriteDuplicateTestNames",
MetricName: "rewrite_duplicate_test_names",
Stage: rewriteDuplicateTestNames,
})
r.compiler.WithStageAfterID(ast.StageRewriteLocalVars, ast.CompilerStageDefinition{
Name: "InjectTestCaseFunc",
MetricName: "inject_test_case_func",
Stage: injectTestCaseFunc,
})
if r.store == nil {
r.store = inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false))
}
if len(r.bundles) > 0 {
if txn == nil {
return nil, errors.New("unable to activate bundles: storage transaction is nil")
}
// Activate the bundle(s) to get their info and policies into the store
// the actual compiled policies will be overwritten later.
opts := &bundle.ActivateOpts{
Ctx: ctx,
Store: r.store,
Txn: txn,
Compiler: r.compiler,
Metrics: metrics.New(),
Bundles: r.bundles,
ParserOptions: ast.ParserOptions{RegoVersion: r.defaultRegoVersion},
}
err := bundle.Activate(opts)
if err != nil {
return nil, err
}
// Aggregate the bundle modules with other ones provided
if r.modules == nil {
r.modules = map[string]*ast.Module{}
}
for path, b := range r.bundles {
maps.Copy(r.modules, b.ParsedModules(path))
}
}
if len(r.modules) > 0 {
if r.compiler.Compile(r.modules); r.compiler.Failed() {
return nil, r.compiler.Errors
}
}
return testRegex, nil
}
func (r *Runner) runTests(ctx context.Context, txn storage.Transaction, enablePrintStatements bool, runFunc run, parallel int) (chan *Result, error) {
testRegex, err := r.setupTestRun(ctx, txn, enablePrintStatements)
if err != nil {
return nil, err
}
ch := make(chan *Result)
go func() {
defer close(ch)
var wg sync.WaitGroup
semaphore := make(chan struct{}, parallel)
stopCtx, cancelTests := context.WithCancel(ctx)
defer cancelTests()
for _, module := range r.compiler.Modules {
// If:
// 1. prefix matchers have been provided, and
// 2. no provided prefix matches the package path, and
// 3. the package path is not a prefix of any provided matcher
// We can skip this the module entirely
if !r.prefixMatchers.Match(module.Package.Path) && !r.prefixMatchers.AnyPrefixMatcher(module.Package.Path) {
continue
}
for _, rule := range module.Rules {
wg.Go(func() {
select {
case <-stopCtx.Done():
return
default:
}
select {
case semaphore <- struct{}{}:
defer func() { <-semaphore }()
case <-stopCtx.Done():
return
}
if !r.shouldRun(rule, testRegex) {
return
}
tr, stop := func() (*Result, bool) {
runCtx, cancel := context.WithTimeout(ctx, r.timeout)
defer cancel()
return runFunc(runCtx, txn, module, rule)
}()
ch <- tr
if stop {
cancelTests()
}
})
}
}
wg.Wait()
}()
return ch, nil
}
func (r *Runner) shouldRun(rule *ast.Rule, testRegex *regexp.Regexp) bool {
ruleRef := rule.Head.Ref().GroundPrefix()
// len check even though the Match function does this already, as we'll
// want to avoid the Extend allocations in the common case of no prefixes configured
if len(r.prefixMatchers) > 0 && !r.prefixMatchers.Match(rule.Module.Package.Path.Extend(ruleRef)) {
return false
}
var ref ast.Ref
for _, term := range ruleRef {
ref = ref.Append(term)
var n string
switch v := term.Value.(type) {
case ast.Var:
n = string(v)
case ast.String:
n = string(v)
}
if strings.HasPrefix(n, TestPrefix) || strings.HasPrefix(n, SkipTestPrefix) {
// Even with the prefix it needs to pass the regex (if applicable)
fullName := rule.Module.Package.Path.Extend(ref).String()
if testRegex != nil && !testRegex.MatchString(fullName) {
return false
}
return true
}
}
return false
}
// rewriteDuplicateTestNames will rewrite duplicate test names to have a numbered suffix.
// This uses a global "count" of each to ensure compiling more than once as new modules
// are added can't introduce duplicates again.
func rewriteDuplicateTestNames(compiler *ast.Compiler) *ast.Error {
count := map[string]int{}
for _, mod := range compiler.Modules {
for _, rule := range mod.Rules {
name, ref := ruleName(rule.Head)
if !strings.HasPrefix(name, TestPrefix) {
continue
}
key := mod.Package.Path.Extend(ref).String()
if k, ok := count[key]; ok {
dynamicSuffix := rule.Head.Ref()[len(ref):]
newName := fmt.Sprintf("%s#%02d", name, k)
if len(ref) == 1 {
ref[0] = ast.VarTerm(newName)
} else {
ref[len(ref)-1] = ast.StringTerm(newName)
}
rule.Head.SetRef(append(ref, dynamicSuffix...))
}
count[key]++
}
}
return nil
}
var testCaseFuncRef = ast.InternalTestCase.Ref()
// injectTestCaseFunc will inject a call to the 'internal.test_case' function into partial-object test rules.
// We attempt to find the earliest point in the rule body where we can inject the call, to ensure that the test-case
// function is called as early as possible so that we capture as many failed test cases as possible.
// This may require us to move generated assignment expressions up the body.
// We do not attempt to move non-generated expressions, as that could contradict author intent.
//
// Consider the test rule:
//
// test_concat[tc.note] if {
// some tc in [{
// "note": "empty + empty",
// "a": [],
// "b": [],
// "exp": [],
// }]
// act := array.concat(tc.a, tc.b)
// act == tc.exp
// }
//
// The compiler will rewrite this rule to (mid-stage @ 'RewriteLocalVars'):
//
// test_concat[__local0__] := true if {
// __local3__ = [{"a": [], "b": [], "exp": [], "note": "empty + empty"}][__local2__]
// __local4__ = array.concat(__local3__.a, __local3__.b)
// __local4__ == __local3__.exp
// __local0__ = __local3__.note # generated var
// }
//
// We move the generated var assignment as far up the body as possible, and inject the test-case function below it:
//
// test_concat[__local0__] := true if {
// __local3__ = [{"a": [], "b": [], "exp": [], "note": "empty + empty"}][__local2__]
// __local0__ = __local3__.note # moved up
// internal.test_case([__local0__]) # injected
// __local4__ = array.concat(__local3__.a, __local3__.b) # this and below expressions can now fail eval and we will still have captured the test-case
// __local4__ == __local3__.exp
// }
func injectTestCaseFunc(compiler *ast.Compiler) *ast.Error {
for _, mod := range compiler.Modules {
for _, rule := range mod.Rules {
// Only apply to test rules
rName, rRef := ruleName(rule.Head)
if !strings.HasPrefix(rName, TestPrefix) {
continue
}
// Only apply to rules that doesn't have manual use of the test-case function
manualCall := false
ast.WalkExprs(rule.Body, func(expr *ast.Expr) bool {
if expr.IsCall() && expr.Operator().Equal(testCaseFuncRef) {
manualCall = true
return true
}
return false
})
if manualCall {
continue
}
// Construct test-case name
ref := rule.Head.Ref()
if len(ref) <= len(rRef) {
// We only inject the test-case function if there is a rule ref "tail" behind the rule name
continue
}
argsRef := ref[len(rRef):]
args := ast.NewArray(argsRef...)
//
// Pass 1: Move generated assignment expressions up the body
//
for _, term := range argsRef {
// We expect to find generated expressions - if any - at the tail of the body, so we start from the end
for i := len(rule.Body) - 1; i >= 0; {
expr := rule.Body[i]
moved := false
// If the expression is a generated assignment of a var in the head ref, we attempt to move it as far
// up the body as possible.
// This is a shallow move, we don't attempt to detect multiple levels of indirection and don't move such expressions; in such case, we move the assigning expression up to the first reference.
// Once done for all vars in the head ref, we can inject the test case function below the last (possibly moved) such expr.
// Note: We don't move non-generated expressions, as that could contradict author intent.
if expr.Generated && (expr.IsEquality() || expr.IsAssignment()) && expr.Operand(0).Equal(term) {
// Based on the vars in the rhs of the expr, see if we can move it up the rule body
// FIXME: Can we get away with just placing it under the lowes first occurrence of any referenced var?
vars := ast.NewVarSet()
ast.WalkVars(expr.Operand(1), func(v ast.Var) bool {
// We only care about local vars
if isLocalVar(v) {
vars.Add(v)
}
return false
})
if len(vars) == 0 {
// No local vars referenced, can be moved to top of body
rule.Body, moved = moveExpr(rule.Body, i, 0)
} else {
// Find the lowest (highest up the body) individual index of each var referenced in the rhs,
// and select the highest (lowest down the body) of those
lowest := util.NewHasherMap[ast.Var, int](cmpEqual)
for j := i - 1; j >= 0; j-- {
expr := rule.Body[j]
ast.WalkVars(expr, func(v ast.Var) bool {
if vars.Contains(v) {
// We override the value for each var, so we get the lowest index
// (line highest up the body) for each
lowest.Put(v, j)
return true
}
return false
})
}
highest := 0
lowest.Iter(func(k ast.Var, v int) bool {
if v > highest {
highest = v
}
return false
})
if highest < i {
// The expression is lower in the body than the lowest line of any expression that might
// contribute to its assignment. Move the expression to just after the lowest line.
rule.Body, moved = moveExpr(rule.Body, i, highest+1)
}
}
}
// If the expression was moved, we need to re-evaluate the current index, as it contains a new expression
if !moved {
i--
}
}
}
//
// Pass 2: Inject the test-case function below the lowest first occurrence of any referenced var
//
injectBelowMap := ast.NewValueMap()
for _, term := range argsRef {
for i, expr := range slices.Backward(rule.Body) {
ast.WalkVars(expr, func(v ast.Var) bool {
if v.Equal(term.Value) {
injectBelowMap.Put(v, ast.Number(strconv.Itoa(i)))
}
return false
})
}
}
// Find the earliest point where the test case function can be injected
injectBelow := -1
injectBelowMap.Iter(func(k, v ast.Value) bool {
if n, err := strconv.Atoi(string(v.(ast.Number))); err == nil {
if n > injectBelow {
injectBelow = n
}
}
return false
})
testCaseFuncExpr := ast.NewExpr([]*ast.Term{
ast.NewTerm(ast.InternalTestCase.Ref()),
ast.NewTerm(args),
})
rule.Body = insertExpr(rule.Body, testCaseFuncExpr, injectBelow+1)
}
}
return nil
}
func cmpEqual[T comparable](a, b T) bool {
return a == b
}
func isLocalVar(v ast.Value) bool {
if v, ok := v.(ast.Var); ok {
if strings.HasPrefix(string(v), ast.LocalVarPrefix) {
return true
}
}
return false
}
func insertExpr(body ast.Body, expr *ast.Expr, index int) ast.Body {
if index <= 0 {
return append(ast.Body{expr}, body...)
}
if index >= len(body) {
return append(body, expr)
}
return append(body[:index], append(ast.Body{expr}, body[index:]...)...)
}
func moveExpr(body ast.Body, from int, to int) (ast.Body, bool) {
if from == to {
return body, false
}
expr := body[from] // Save the expression to move
body = slices.Delete(body, from, from+1) // Remove the expression from the body
body = append(body[:to], append(ast.Body{expr}, body[to:]...)...) // Insert the expression at the new position
return body, true
}
// ruleName is a helper to be used when checking if a function
// (a) is a test, or
// (b) needs to be skipped
// -- it'll resolve `p.q.r` to `r`. For representing results, we'll
// use rule.Head.Ref()
func ruleName(h *ast.Head) (string, ast.Ref) {
var n string
rgp := h.Ref().GroundPrefix()
i := 0
for i = range rgp {
switch v := rgp[i].Value.(type) {
case ast.Var:
n = string(v)
case ast.String:
n = string(v)
default:
n = ""
}
if strings.HasPrefix(n, TestPrefix) || strings.HasPrefix(n, SkipTestPrefix) {
break
}
}
return n, rgp[:i+1]
}
func (r *Runner) runTest(ctx context.Context, txn storage.Transaction, mod *ast.Module, rule *ast.Rule) (*Result, bool) {
ruleName, ruleRef := ruleName(rule.Head)
if strings.HasPrefix(ruleName, SkipTestPrefix) { // TODO(sr): add test
tr := newResult(rule.Loc(), mod.Package.Path.String(), ruleRef.String(), 0*time.Second, nil, nil)
tr.Skip = true
return tr, false
}
var bufferTracer *topdown.BufferTracer
var tracers []topdown.QueryTracer
if r.trace {
bufferTracer = topdown.NewBufferTracer()
tracers = append(tracers, bufferTracer)
} else {
t := NewTestQueryTracer()
tracers = append(tracers, t)
bufferTracer = &t.BufferTracer
}
if r.cover != nil {
tracers = append(tracers, r.cover)
}
printbuf := bytes.NewBuffer(nil)
var builtinErrors []topdown.Error
queryPath := rule.Module.Package.Path.Extend(ruleRef)
opts := []func(*rego.Rego){
rego.Store(r.store),
rego.Transaction(txn),
rego.Compiler(r.compiler),
rego.Query(queryPath.String()),
rego.Runtime(r.runtime),
rego.Target(r.target),
rego.PrintHook(topdown.NewPrintHook(printbuf)),
rego.BuiltinErrorList(&builtinErrors),
}
for _, t := range tracers {
opts = append(opts, rego.QueryTracer(t))
}
rg := rego.New(opts...)
// Register custom builtins on rego instance
for _, v := range r.customBuiltins {
v.Func(rg)
}
ctx = ast.WithCompiler(ctx, r.compiler)
t0 := time.Now()
rs, err := rg.Eval(ctx)
dt := time.Since(t0)
var trace []*topdown.Event
if bufferTracer != nil {
trace = *bufferTracer
}
tr := newResult(rule.Loc(), mod.Package.Path.String(), ruleRef.String(), dt, trace, printbuf.Bytes())
// If there was an error other than errors from builtins, prefer that error.
if err != nil {
tr.Error = err
} else if r.raiseBuiltinErrors && len(builtinErrors) > 0 {
if len(builtinErrors) == 1 {
tr.Error = &builtinErrors[0]
} else {
tr.Error = fmt.Errorf("%v", builtinErrors)
}
}
var stop bool
if err != nil {
if topdown.IsCancel(err) || wasm_errors.IsCancel(err) {
stop = ctx.Err() != context.DeadlineExceeded
}
} else if len(rs) == 0 {
tr.Fail = true
} else if rule.Head.DocKind() == ast.PartialObjectDoc {
tr.Fail, tr.SubResults = subResults(rs[0].Expressions[0].Value, trace)
} else if b, ok := rs[0].Expressions[0].Value.(bool); !ok || !b {
tr.Fail = true
}
return tr, stop
}
func subResults(v any, trace []*topdown.Event) (bool, map[string]*SubResult) {
if v == nil {
return true, map[string]*SubResult{}
}
var fail bool
result := SubResultMap{}
switch x := v.(type) {
case map[string]any:
for k, v := range x {
sr := subResult(k, v)
result[k] = sr
if sr.Fail {
fail = true
}
}
}
// Create failed sub-results and apply per-test-case traces.
// For each test-case event, we capture the trace from first event up until the next test-case event.
var testEvent *topdown.Event
for i, e := range trace {
if e.Op == topdown.TestCaseOp {
if testEvent != nil {
if p, ok := testCaseTerms(testEvent); ok {
if f := result.Update(*p, trace[:i]); f {
fail = true
}
}
}
testEvent = e
}
}
if testEvent != nil {
if p, ok := testCaseTerms(testEvent); ok {
if f := result.Update(*p, trace); f {
fail = true
}
}
}
return fail, result
}
func testCaseTerms(e *topdown.Event) (*ast.Array, bool) {
if e == nil {
return nil, false
}
if expr, ok := e.Node.(*ast.Expr); ok {
if arr, ok := expr.Operand(0).Value.(*ast.Array); ok {
return arr, true
}
}
return nil, false
}
func subResult(n string, v any) *SubResult {
if v == nil {
return &SubResult{}
}
switch x := v.(type) {
case map[string]any:
fail, srs := subResults(x, nil)
return &SubResult{
Name: n,
Fail: fail,
SubResults: srs,
}
case bool:
return &SubResult{
Name: n,
Fail: !x,
}
default:
return &SubResult{
Name: n,
Fail: true,
}
}
}
func (r *Runner) runBenchmark(ctx context.Context, txn storage.Transaction, mod *ast.Module, rule *ast.Rule, options BenchmarkOptions) (*Result, bool) {
_, rf := ruleName(rule.Head)
tr := &Result{
Location: rule.Loc(),
Package: mod.Package.Path.String(),
Name: rf.String(), // TODO(sr): test
}
var stop bool
t0 := time.Now()
br := testing.Benchmark(func(b *testing.B) {
pq, err := rego.New(
rego.Store(r.store),
rego.Transaction(txn),
rego.Compiler(r.compiler),
rego.Query(rule.Module.Package.Path.Extend(rule.Head.Ref().GroundPrefix()).String()),
rego.Runtime(r.runtime),
rego.Target(r.target),
).PrepareForEval(ctx)
if err != nil {
tr.Fail = true
b.Fatalf("Unexpected error: %s", err)
}
m := metrics.New()
// Track memory allocations
if options.ReportAllocations {
b.ReportAllocs()
}
for b.Loop() {
opts := []rego.EvalOption{rego.EvalTransaction(txn), rego.EvalMetrics(m)}
var tracer *TestQueryTracer
if rule.Head.DocKind() == ast.PartialObjectDoc {
tracer = NewTestQueryTracer()
opts = append(opts, rego.EvalQueryTracer(tracer))
}
rs, err := pq.Eval(ctx, opts...)
if err != nil {
tr.Error = err
if topdown.IsCancel(err) && !(ctx.Err() == context.DeadlineExceeded) {
stop = true
}
b.Fatalf("Unexpected error: %s", err)
} else if len(rs) == 0 {
tr.Fail = true
b.Fatal("Expected boolean result, got `undefined`")
} else if rule.Head.DocKind() == ast.PartialObjectDoc {
tr.Fail, tr.SubResults = subResults(rs[0].Expressions[0].Value, tracer.Events())
} else if pass, ok := rs[0].Expressions[0].Value.(bool); !ok || !pass {
tr.Fail = true
b.Fatal("Expected test to evaluate as true, got false")
}
}
for k, v := range m.All() {
var val float64
switch v := v.(type) {
case int64:
val = float64(v)
case uint64:
val = float64(v)
case float64:
val = v
default:
continue // skip this metric
}
fv := val / float64(b.N)
b.ReportMetric(fv, k+"/op")
}
})
tr.Duration = time.Since(t0)
tr.BenchmarkResult = &br
return tr, stop
}
// Load returns modules and an in-memory store for running tests.
func Load(args []string, filter loader.Filter) (map[string]*ast.Module, storage.Store, error) {
return LoadWithRegoVersion(args, filter, ast.DefaultRegoVersion)
}
// LoadWithRegoVersion returns modules and an in-memory store for running tests.
// Modules are parsed in accordance with the given RegoVersion.
func LoadWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*ast.Module, storage.Store, error) {
if regoVersion == ast.RegoUndefined {
regoVersion = ast.DefaultRegoVersion
}
loaded, err := loader.NewFileLoader().
WithRegoVersion(regoVersion).
WithBundleLazyLoadingMode(bundle.HasExtension()).
WithProcessAnnotation(true).
Filtered(args, filter)
if err != nil {
return nil, nil, err
}
var store storage.Store
ctx := context.Background()
if bundle.BundleExtStore != nil {
store = bundle.BundleExtStore()
// inline'd NewFromObject
if err := storage.WriteOne(ctx, store, storage.AddOp, storage.RootPath, loaded.Documents); err != nil {
return nil, nil, err
}
} else {
store = inmem.NewFromObject(loaded.Documents)
}
modules := make(map[string]*ast.Module, len(loaded.Modules))
err = storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
for _, loadedModule := range loaded.Modules {
modules[loadedModule.Name] = loadedModule.Parsed
// Add the policies to the store to ensure that any future bundle
// activations will preserve them and re-compile the module with
// the bundle modules.
err := store.UpsertPolicy(ctx, txn, loadedModule.Name, loadedModule.Raw)
if err != nil {
return err
}
}
return nil
})
return modules, store, err
}
// LoadWithParserOptions returns modules and an in-memory store for running tests.
// Modules are parsed in accordance with the given [ast.ParserOptions].
func LoadWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*ast.Module, storage.Store, error) {
loaded, err := loader.NewFileLoader().
WithRegoVersion(popts.RegoVersion).
WithCapabilities(popts.Capabilities).
WithProcessAnnotation(popts.ProcessAnnotation).
WithBundleLazyLoadingMode(bundle.HasExtension()).
Filtered(args, filter)
if err != nil {
return nil, nil, err
}
var store storage.Store
ctx := context.Background()
// Plumb in storage for external bundle activation plugin, if registered with bundle.RegisterStore.
if bundle.BundleExtStore != nil {
store = bundle.BundleExtStore()
// inline'd NewFromObject
if err := storage.WriteOne(ctx, store, storage.AddOp, storage.RootPath, loaded.Documents); err != nil {
return nil, nil, err
}
} else {
store = inmem.NewFromObject(loaded.Documents)
}
modules := make(map[string]*ast.Module, len(loaded.Modules))
err = storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
for _, loadedModule := range loaded.Modules {
modules[loadedModule.Name] = loadedModule.Parsed
// Add the policies to the store to ensure that any future bundle
// activations will preserve them and re-compile the module with
// the bundle modules.
if err := store.UpsertPolicy(ctx, txn, loadedModule.Name, loadedModule.Raw); err != nil {
return err
}
}
return nil
})
return modules, store, err
}
// LoadBundles will load the given args as bundles, either tarball or directory is OK.
func LoadBundles(args []string, filter loader.Filter) (map[string]*bundle.Bundle, error) {
return LoadBundlesWithRegoVersion(args, filter, ast.RegoV0)
}
// LoadBundlesWithRegoVersion will load the given args as bundles, either tarball or directory is OK.
// Bundles are parsed in accordance with the given RegoVersion.
func LoadBundlesWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*bundle.Bundle, error) {
if regoVersion == ast.RegoUndefined {
regoVersion = ast.DefaultRegoVersion
}
bundles := make(map[string]*bundle.Bundle, len(args))
for _, bundleDir := range args {
b, err := loader.NewFileLoader().
WithRegoVersion(regoVersion).
WithProcessAnnotation(true).
WithSkipBundleVerification(true).
WithBundleLazyLoadingMode(bundle.HasExtension()).
WithFilter(filter).
AsBundle(bundleDir)
if err != nil {
return nil, fmt.Errorf("unable to load bundle %s: %s", bundleDir, err)
}
bundles[bundleDir] = b
}
return bundles, nil
}
// LoadBundlesWithParserOptions will load the given args as bundles, either tarball or directory is OK.
// Bundles are parsed in accordance with the given [ast.ParserOptions].
func LoadBundlesWithParserOptions(args []string, filter loader.Filter, popts ast.ParserOptions) (map[string]*bundle.Bundle, storage.Store, error) {
if popts.RegoVersion == ast.RegoUndefined {
popts.RegoVersion = ast.DefaultRegoVersion
}
bundles := make(map[string]*bundle.Bundle, len(args))
for _, bundleDir := range args {
b, err := loader.NewFileLoader().
WithRegoVersion(popts.RegoVersion).
WithCapabilities(popts.Capabilities).
WithProcessAnnotation(popts.ProcessAnnotation).
WithSkipBundleVerification(true).
WithBundleLazyLoadingMode(bundle.HasExtension()).
WithFilter(filter).
AsBundle(bundleDir)
if err != nil {
return nil, nil, fmt.Errorf("unable to load bundle %s: %s", bundleDir, err)
}
bundles[bundleDir] = b
}
// Plumb in storage for external bundle activation plugin, if registered with bundle.RegisterStore.
if bundle.BundleExtStore != nil {
return bundles, bundle.BundleExtStore(), nil
}
return bundles, inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false)), nil
}