Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tools/celtest/test_coverage_reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
95 changes: 64 additions & 31 deletions tools/celtest/test_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 5 additions & 4 deletions tools/celtest/test_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
})
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
)
Expand Down
13 changes: 10 additions & 3 deletions tools/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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()...)
Expand Down