diff --git a/tools/celtest/test_coverage_reporter_test.go b/tools/celtest/test_coverage_reporter_test.go index 507173a0..ae7b0c70 100644 --- a/tools/celtest/test_coverage_reporter_test.go +++ b/tools/celtest/test_coverage_reporter_test.go @@ -87,9 +87,9 @@ func TestCoverageStats(t *testing.T) { tc.compilerOpts = append(tc.compilerOpts, envOpt) } compilerOpt := TestCompiler(tc.compilerOpts...) - testSuiteParserOpt := DefaultTestSuiteParser(tc.testSuitePath) + testSuiteOpt := TestSuite(tc.testSuitePath) testExprOpt := TestExpression(tc.celExpr) - tr, err := NewTestRunner(compilerOpt, testSuiteParserOpt, testExprOpt, EnableCoverage(), PartialEvalProgramOption()) + tr, err := NewTestRunner(compilerOpt, testSuiteOpt, testExprOpt, EnableCoverage(), PartialEvalProgramOption()) if err != nil { t.Fatalf("compiler.NewCompiler() failed: %v", err) } diff --git a/tools/celtest/test_runner.go b/tools/celtest/test_runner.go index ea3ed3e9..b27f02e0 100644 --- a/tools/celtest/test_runner.go +++ b/tools/celtest/test_runner.go @@ -187,8 +187,8 @@ func TestRunnerOptionsFromFlags(testResourcesDir string, testRunnerOpts []TestRu return func(tr *TestRunner) (*TestRunner, error) { opts := []TestRunnerOption{ testRunnerCompilerFromFlags(testCompilerOpts...), - DefaultTestSuiteParser(testSuitePath), - AddFileDescriptorSet(fileDescriptorSetPath), + TestSuite(testSuitePath), + FileDescriptorSet(fileDescriptorSetPath), TestExpression(celExpression), } if enableCoverage { @@ -224,6 +224,18 @@ func testRunnerCompilerFromFlags(testCompilerOpts ...any) TestRunnerOption { return TestCompiler(opts...) } +// ActivationFactory is a function which creates a CEL activation from the provided variables. +type ActivationFactory func(vars any) (cel.Activation, error) + +// TestInputActivationFactory updates the test runner with the provided activation factory. +// The activation factory is used to create a CEL activation from the provided variables. +func TestInputActivationFactory(f ActivationFactory) TestRunnerOption { + return func(tr *TestRunner) (*TestRunner, error) { + tr.activationFactory = f + return tr, nil + } +} + // TestSuiteParser is an interface for parsing a test suite: // - ParseTextproto: Returns a cel.spec.expr.conformance.test.TestSuite message. // - ParseYAML: Returns a test.Suite object. @@ -272,8 +284,17 @@ func (p *tsParser) ParseYAML(path string) (*test.Suite, error) { return testSuite, err } +// TestSuite provides a reference to the test suite file (either .yaml or .textproto) +func TestSuite(path string) TestRunnerOption { + return func(tr *TestRunner) (*TestRunner, error) { + tr.TestSuiteFilePath = path + return tr, nil + } +} + // DefaultTestSuiteParser returns a TestRunnerOption which configures the test runner with // the default test suite parser. +// Deprecated: prefer TestSuite(path) func DefaultTestSuiteParser(path string) TestRunnerOption { return func(tr *TestRunner) (*TestRunner, error) { if path == "" { @@ -310,12 +331,14 @@ func TestSuiteParserOption(p TestSuiteParser) TestRunnerOption { // - ExecuteTest: Executes a single type TestRunner struct { compiler.Compiler - Expressions []compiler.InputExpression - TestSuiteFilePath string - FileDescriptorSetPath string - testSuiteParser TestSuiteParser - testProgramOptions []cel.ProgramOption - EnableCoverage bool + Expressions []compiler.InputExpression + TestSuiteFilePath string + FileDescriptorSet *descpb.FileDescriptorSet + EnableCoverage bool + + activationFactory ActivationFactory + testSuiteParser TestSuiteParser + testProgramOptions []cel.ProgramOption } // Test represents a single test case to be executed. It encompasses the following: @@ -325,12 +348,12 @@ type TestRunner struct { // returns a TestResult. type Test struct { name string - input interpreter.PartialActivation + input cel.PartialActivation resultMatcher func(ref.Val, error) TestResult } // NewTest creates a new Test with the provided name, input and result matcher. -func NewTest(name string, input interpreter.PartialActivation, resultMatcher func(ref.Val, error) TestResult) *Test { +func NewTest(name string, input cel.PartialActivation, resultMatcher func(ref.Val, error) TestResult) *Test { return &Test{ name: name, input: input, @@ -354,7 +377,10 @@ type TestResult struct { // - configure the Compiler used for parsing and compiling the input expressions // - configure the Test Runner used for parsing and executing the tests func NewTestRunner(opts ...TestRunnerOption) (*TestRunner, error) { - tr := &TestRunner{} + tr := &TestRunner{ + activationFactory: cel.NewActivation, + testSuiteParser: &tsParser{}, + } var err error for _, opt := range opts { tr, err = opt(tr) @@ -402,25 +428,32 @@ func CustomTestCompiler(c compiler.Compiler) TestRunnerOption { } } -// AddFileDescriptorSet creates a Test Runner Option which adds a file descriptor set to the test +// FileDescriptorSet creates a Test Runner Option which adds a file descriptor set to the test // runner. The file descriptor set is used to register proto messages in the global proto registry. -func AddFileDescriptorSet(path string) TestRunnerOption { +func FileDescriptorSet(path string) TestRunnerOption { return func(tr *TestRunner) (*TestRunner, error) { if path != "" { - tr.FileDescriptorSetPath = path + fds, err := fileDescriptorSet(path) + if err != nil { + return nil, err + } + tr.FileDescriptorSet = fds } return tr, nil } } -func registerMessages(path string) error { - if path == "" { - return nil - } - fds, err := fileDescriptorSet(path) - if err != nil { - return err +// AddFileDescriptorSetProto creates a Test Runner Option which adds a file descriptor set proto to +// the test runner. The file descriptor set is used to register proto messages in the global proto +// registry. +func AddFileDescriptorSetProto(fds *descpb.FileDescriptorSet) TestRunnerOption { + return func(tr *TestRunner) (*TestRunner, error) { + tr.FileDescriptorSet = fds + return tr, nil } +} + +func registerMessages(fds *descpb.FileDescriptorSet) error { for _, file := range fds.GetFile() { reflectFD, err := protodesc.NewFile(file, protoregistry.GlobalFiles) if err != nil { @@ -547,9 +580,9 @@ func (tr *TestRunner) Tests(t *testing.T) ([]*Test, error) { } else if testSuite != nil { return tr.createTestsFromYAML(t, testSuite) } - err := registerMessages(tr.FileDescriptorSetPath) + err := registerMessages(tr.FileDescriptorSet) if err != nil { - return nil, fmt.Errorf("registerMessages(%q) failed: %v", tr.FileDescriptorSetPath, err) + return nil, fmt.Errorf("registerMessages(%v) failed: %v", tr.FileDescriptorSet, err) } if testSuite, err := tr.testSuiteParser.ParseTextproto(tr.TestSuiteFilePath); err != nil && !strings.Contains(err.Error(), "invalid file extension") { @@ -580,14 +613,14 @@ func (tr *TestRunner) createTestsFromTextproto(t *testing.T, testSuite *conforma return tests, nil } -func (tr *TestRunner) createTestInputFromPB(t *testing.T, testCase *conformancepb.TestCase) (interpreter.PartialActivation, error) { +func (tr *TestRunner) createTestInputFromPB(t *testing.T, testCase *conformancepb.TestCase) (cel.PartialActivation, error) { t.Helper() input := map[string]any{} e, err := tr.CreateEnv() if err != nil { return nil, err } - var activation interpreter.Activation + var activation cel.Activation if testCase.GetInputContext() != nil { if len(testCase.GetInput()) != 0 { return nil, fmt.Errorf("only one of input and input_context can be provided at a time") @@ -634,9 +667,9 @@ func (tr *TestRunner) createTestInputFromPB(t *testing.T, testCase *conformancep } } } - activation, err = interpreter.NewActivation(input) + activation, err = tr.activationFactory(input) if err != nil { - return nil, fmt.Errorf("interpreter.NewActivation(%q) failed: %w", input, err) + return nil, fmt.Errorf("activationFactory(%q) failed: %w", input, err) } return e.PartialVars(activation) } @@ -812,13 +845,13 @@ func (tr *TestRunner) createTestsFromYAML(t *testing.T, testSuite *test.Suite) ( return tests, nil } -func (tr *TestRunner) createTestInput(t *testing.T, testCase *test.Case) (interpreter.PartialActivation, error) { +func (tr *TestRunner) createTestInput(t *testing.T, testCase *test.Case) (cel.PartialActivation, error) { t.Helper() e, err := tr.CreateEnv() if err != nil { return nil, err } - var activation interpreter.Activation + var activation cel.Activation if testCase.InputContext != nil && testCase.InputContext.ContextExpr != "" { if len(testCase.Input) != 0 { return nil, fmt.Errorf("only one of input and input_context can be provided at a time") @@ -850,9 +883,9 @@ func (tr *TestRunner) createTestInput(t *testing.T, testCase *test.Case) (interp } input[k] = v.Value } - activation, err = interpreter.NewActivation(input) + activation, err = tr.activationFactory(input) if err != nil { - return nil, fmt.Errorf("interpreter.NewActivation(%q) failed: %w", input, err) + return nil, fmt.Errorf("activationFactory(%q) failed: %w", input, err) } return e.PartialVars(activation) } diff --git a/tools/celtest/test_runner_test.go b/tools/celtest/test_runner_test.go index 123c10f1..deb6699a 100644 --- a/tools/celtest/test_runner_test.go +++ b/tools/celtest/test_runner_test.go @@ -109,14 +109,14 @@ func k8sParserOpts() policy.ParserOption { func TestTriggerTestsWithRunnerOptions(t *testing.T) { t.Run("test trigger tests custom policy", func(t *testing.T) { envOpt := compiler.EnvironmentFile("../../policy/testdata/k8s/config.yaml") - testSuiteParser := DefaultTestSuiteParser("../../policy/testdata/k8s/tests.yaml") + testSuite := TestSuite("../../policy/testdata/k8s/tests.yaml") testCELPolicy := TestExpression("../../policy/testdata/k8s/policy.yaml") c, err := compiler.NewCompiler(envOpt, k8sParserOpts()) if err != nil { t.Fatalf("compiler.NewCompiler() failed: %v", err) } compilerOpt := CustomTestCompiler(c) - opts := []TestRunnerOption{compilerOpt, testSuiteParser, testCELPolicy} + opts := []TestRunnerOption{compilerOpt, testSuite, testCELPolicy} TriggerTests(t, opts...) }) } @@ -127,6 +127,7 @@ func customPolicyParserOption() policy.ParserOption { return p, nil } } + func ParsePolicyVariables(metadata map[string]any) cel.EnvOption { var variables []*decls.VariableDecl for n, t := range metadata { @@ -197,8 +198,8 @@ func TestTriggerTests(t *testing.T) { } testOpts = append(testOpts, TestCompiler(compileOpts...), - DefaultTestSuiteParser(tc.testSuitePath), - AddFileDescriptorSet(tc.fileDescriptorSetPath), + FileDescriptorSet(tc.fileDescriptorSetPath), + TestSuite(tc.testSuitePath), TestExpression(tc.celExpression), PartialEvalProgramOption(), ) diff --git a/tools/compiler/compiler.go b/tools/compiler/compiler.go index 3e88ef2c..47e1fca7 100644 --- a/tools/compiler/compiler.go +++ b/tools/compiler/compiler.go @@ -97,6 +97,11 @@ type Compiler interface { CreatePolicyParser() (*policy.Parser, error) // PolicyCompilerOptions returns the policy compiler options. PolicyCompilerOptions() []policy.CompilerOption +} + +// CustomMetadataCompiler interface is used to set up a compiler with the following capabilities: +// - fetch policy metadata environment options +type CustomMetadataCompiler interface { // PolicyMetadataEnvOptions returns the policy metadata environment options. PolicyMetadataEnvOptions() []PolicyMetadataEnvOption } @@ -522,9 +527,11 @@ func (f *FileExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any, return nil, nil, fmt.Errorf("parser.Parse(%q) failed: %w", src.Content(), iss.Err()) } policyMetadata := clonePolicyMetadata(p) - for _, opt := range compiler.PolicyMetadataEnvOptions() { - if e, err = e.Extend(opt(policyMetadata)); err != nil { - return nil, nil, fmt.Errorf("e.Extend() with metadata option failed: %w", err) + if meta, ok := compiler.(CustomMetadataCompiler); ok { + for _, opt := range meta.PolicyMetadataEnvOptions() { + if e, err = e.Extend(opt(policyMetadata)); err != nil { + return nil, nil, fmt.Errorf("e.Extend() with metadata option failed: %w", err) + } } } ast, iss := policy.Compile(e, p, compiler.PolicyCompilerOptions()...)