Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a30d67d
Add k8s custom policy tag handler for test
aakash070 Feb 4, 2025
77aab71
added copyright and removed redundant attribute from go_library target
aakash070 Feb 5, 2025
9e155af
refactor code to extract common methods
aakash070 Mar 10, 2025
73bba9e
reverted redundant code
aakash070 Mar 11, 2025
7afe78c
added compiler tool
aakash070 Mar 12, 2025
d261979
replaced textproto file with an in memory pb object
aakash070 Mar 21, 2025
6cb138d
reverted update to root go.mod
aakash070 Mar 25, 2025
5cab162
added cel.dev/expr/conformance to vendor
aakash070 Mar 25, 2025
907b6c3
add vendor to tools
aakash070 Mar 25, 2025
09f6866
deleted tools/vendor
aakash070 Mar 25, 2025
bf7624c
Add test runner library
aakash070 Apr 2, 2025
afdf755
created test runner option for test suite parser
aakash070 Apr 2, 2025
e6028d1
update test suite structure
aakash070 Apr 4, 2025
98be9bd
fix test suite path used to set up test suite parser
aakash070 Apr 4, 2025
dcfc1a0
updated cel.dev/expr version in WORKSPACE
aakash070 Apr 7, 2025
a8a77f5
updated go registered toolchain version in WORKSPACE
aakash070 Apr 7, 2025
3c58df5
updated checksum for cel.dev/expr in WORKSPACE
aakash070 Apr 7, 2025
b867b8a
update cel.dev/expr release version to resolve failing tests
aakash070 Apr 7, 2025
9b8248c
fix vendor verification error
aakash070 Apr 7, 2025
bf96c47
resolved pr comments on YAML test suite and test runner
aakash070 Apr 8, 2025
449ba4f
update test runner to accept any type options instead of TestRunnerOp…
aakash070 Apr 8, 2025
b8c7496
replaced functional test suite parsers with an interface
aakash070 Apr 9, 2025
f942827
update test suite schema in policy testdata files
aakash070 Apr 10, 2025
61399be
added sync once as a compiler object level variable
aakash070 Apr 10, 2025
2dad2f3
fix test failure
aakash070 Apr 10, 2025
c101087
added test for test runner for standard policy
aakash070 Apr 10, 2025
156addf
added tests for custom policy parser via flags
aakash070 Apr 11, 2025
f2022c3
moved test for custom policy from compiler test to test runner test
aakash070 Apr 14, 2025
59cf7ee
changed argument type for Test Suite Parser methods to string
aakash070 Apr 14, 2025
70d7f5f
added tests for raw CEL expression and CEL expression file
aakash070 Apr 14, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
replaced functional test suite parsers with an interface
  • Loading branch information
aakash070 committed Apr 15, 2025
commit b8c7496d801c2085516fdeab62fde502dcff1e21
199 changes: 86 additions & 113 deletions tools/celtest/test_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import (
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/testing/protocmp"

Expand All @@ -48,8 +47,6 @@ import (

var (
celExpression string
celExpressionType int
expressionType compiler.ExpressionType
testSuitePath string
fileDescriptorSetPath string
configPath string
Expand All @@ -62,21 +59,7 @@ func init() {
flag.StringVar(&configPath, "config_path", "", "path to a config file")
flag.StringVar(&baseConfigPath, "base_config_path", "", "path to a base config file")
flag.StringVar(&celExpression, "cel_expr", "", "CEL expression to test")
flag.IntVar(&celExpressionType, "cel_expression_type", 0, "type of the CEL expression")
flag.Parse()
expressionType = compiler.ExpressionType(celExpressionType)
}

func loadInput(path string, format compiler.FileFormat, out protoreflect.ProtoMessage) error {
unmarshaller := proto.Unmarshal
if format == compiler.TextProto {
unmarshaller = prototext.Unmarshal
}
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %q: %v", path, err)
}
return unmarshaller(data, out)
}

// TestRunnerOption is used to configure the following attributes of the Test Runner:
Expand All @@ -86,30 +69,12 @@ func loadInput(path string, format compiler.FileFormat, out protoreflect.ProtoMe
// - set the test suite parser based on the file format: YAML or Textproto
type TestRunnerOption func(*TestRunner) (*TestRunner, error)

// TestSuiteParserTextproto returns a cel.expr.conformance.test.TestSuite
// message which is used to set up Tests.
// - In case the message is serialized in a Textproto file, TestSuiteParserTextproto
// is invoked with the path of the file passed as an argument.
// - Alternatively, TestSuiteParserTextproto can also be invoked to generate the
// cel.expr.conformance.test.TestSuite message at runtime.
type TestSuiteParserTextproto func(*testing.T, any) (*conformancepb.TestSuite, error)

// TestSuiteParserYAML returns a test.TestSuite object which is used to set up Tests.
// - In case the object is serialized in a YAML file, TestSuiteParserYAML is invoked
// with the path of the file passed as an argument.
// - Alternatively, TestSuiteParserYAML can also be invoked to generate the test.TestSuite
// object at runtime.
//
// Note: If TestSuiteParserTextproto is already configured then TestSuiteParserYAML
// will not be invoked.
type TestSuiteParserYAML func(*testing.T, any) (*test.Suite, error)

// TriggerTests triggers tests for a CEL policy, expression or checked expression
// with the provided set of options. The options can be used to:
// - configure the Compiler used for parsing and compiling the expression
// - configure the Test Runner used for parsing and executing the tests
func TriggerTests(t *testing.T, opts ...any) {
testRunnerOptions := testRunnerOptions(opts...)
func TriggerTests(t *testing.T, testRunnerOpts []TestRunnerOption, testCompilerOpts ...any) {
testRunnerOptions := testRunnerOptions(testRunnerOpts, testCompilerOpts...)
tr, err := NewTestRunner(testRunnerOptions...)
if err != nil {
t.Fatalf("error creating test runner: %v", err)
Expand All @@ -132,18 +97,9 @@ func TriggerTests(t *testing.T, opts ...any) {
}
}

func testRunnerOptions(opts ...any) []TestRunnerOption {
testRunnerOpts := make([]TestRunnerOption, 0, len(opts))
testCompilerOpts := make([]any, 0, len(opts))
for _, opt := range opts {
if _, ok := opt.(TestRunnerOption); ok {
testRunnerOpts = append(testRunnerOpts, opt.(TestRunnerOption))
} else {
testCompilerOpts = append(testCompilerOpts, opt)
}
}
func testRunnerOptions(testRunnerOpts []TestRunnerOption, testCompilerOpts ...any) []TestRunnerOption {
compilerOpt := testRunnerCompilerFromFlags(testCompilerOpts...)
testSuiteParserOpt := TestSuiteParser(testSuitePath)
testSuiteParserOpt := DefaultTestSuiteParser(testSuitePath)
fileDescriptorSetOpt := AddFileDescriptorSet(fileDescriptorSetPath)
testRunnerExprOpt := testRunnerExpressionsFromFlags()
return append([]TestRunnerOption{compilerOpt, testSuiteParserOpt, fileDescriptorSetOpt, testRunnerExprOpt}, testRunnerOpts...)
Expand Down Expand Up @@ -174,38 +130,72 @@ func testRunnerCompilerFromFlags(testCompilerOpts ...any) TestRunnerOption {
func testRunnerExpressionsFromFlags() TestRunnerOption {
return func(tr *TestRunner) (*TestRunner, error) {
if celExpression != "" {
switch expressionType {
case compiler.CompiledExpressionFile:
tr.Expressions = append(tr.Expressions, &compiler.CompiledExpression{Path: celExpression})
case compiler.PolicyFile, compiler.ExpressionFile:
tr.Expressions = append(tr.Expressions, &compiler.FileExpression{Path: celExpression})
case compiler.RawExpressionString:
tr.Expressions = append(tr.Expressions, &compiler.RawExpression{Value: celExpression})
default:
return nil, fmt.Errorf("unsupported expression type: %v", expressionType)
}
tr.Expressions = append(tr.Expressions, &compiler.CompiledExpression{Path: celExpression})
tr.Expressions = append(tr.Expressions, &compiler.FileExpression{Path: celExpression})
tr.Expressions = append(tr.Expressions, &compiler.RawExpression{Value: celExpression})
}
return tr, nil
}
}

// TestSuiteParser provides a TestRunnerOption that sets the test suite file path and the test
// suite parser based on the file format: YAML or Textproto.
func TestSuiteParser(path string) TestRunnerOption {
// 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.
// In case the test suite is serialized in a Textproto/YAML file, the path of the file is passed as
// an argument to the parse method.
type TestSuiteParser interface {
ParseTextproto(any) (*conformancepb.TestSuite, error)
ParseYAML(any) (*test.Suite, error)
}

type tsParser struct {
TestSuiteParser
}

// ParseTextproto parses a test suite file in Textproto format.
func (p *tsParser) ParseTextproto(path any) (*conformancepb.TestSuite, error) {
filePath := path.(string)
if filePath == "" {
return nil, nil
}
if fileFormat := compiler.InferFileFormat(filePath); fileFormat != compiler.TextProto {
return nil, fmt.Errorf("invalid file extension wanted: .textproto: found %v", fileFormat)
}
testSuite := &conformancepb.TestSuite{}
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("runfiles.ReadFile(%q) failed: %v", path, err)
}
err = prototext.Unmarshal(data, testSuite)
return testSuite, err
}

// ParseYAML parses a test suite file in YAML format.
func (p *tsParser) ParseYAML(path any) (*test.Suite, error) {
filePath := path.(string)
if filePath == "" {
return nil, nil
}
if fileFormat := compiler.InferFileFormat(filePath); fileFormat != compiler.TextYAML {
return nil, fmt.Errorf("invalid file extension wanted: .yaml: found %v", fileFormat)
}
testSuiteBytes, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("runfiles.ReadFile(%q) failed: %v", filePath, err)
}
testSuite := &test.Suite{}
err = yaml.Unmarshal(testSuiteBytes, testSuite)
return testSuite, err
}

// DefaultTestSuiteParser returns a TestRunnerOption which configures the test runner with a test suite parser.
func DefaultTestSuiteParser(path string) TestRunnerOption {
return func(tr *TestRunner) (*TestRunner, error) {
if testSuitePath == "" {
if path == "" {
return tr, nil
}
tr.TestSuiteFilePath = path
testSuiteFormat := compiler.InferFileFormat(testSuitePath)
switch testSuiteFormat {
case compiler.TextProto:
tr.TestSuiteParserTextproto = defaultTestSuiteParserTextproto
case compiler.TextYAML:
tr.TestSuiteParserYAML = defaultTestSuiteParserYAML
default:
return nil, fmt.Errorf("unsupported test suite file format: %v", testSuiteFormat)
}
tr.testSuiteParser = &tsParser{}
return tr, nil
}
}
Expand All @@ -216,22 +206,18 @@ func TestSuiteParser(path string) TestRunnerOption {
// - Input Expressions: The list of input expressions to be tested.
// - Test Suite File Path: The path to the test suite file.
// - File Descriptor Set Path: The path to the file descriptor set file.
// - Test Suite Parser Textproto: A parser for a custom test suite file serialized in Textproto
// format. This option is required if the provided test suite is not a cel.spec.expr.conformance.test.TestSuite message.
// - Test Suite Parser YAML: A parser for a custom test suite file serialized in YAML format.
// This option is required if the provided test suite is not a test.TestSuite object.
// - test Suite Parser: A parser for a test suite file serialized in Textproto/YAML format.
//
// The TestRunner provides the following methods:
// - Programs: Creates a list of CEL programs from the input expressions.
// - Tests: Creates a list of tests from the test suite file.
// - ExecuteTest: Executes a single
type TestRunner struct {
compiler.Compiler
Expressions []compiler.InputExpression
TestSuiteFilePath string
FileDescriptorSetPath string
TestSuiteParserTextproto TestSuiteParserTextproto
TestSuiteParserYAML TestSuiteParserYAML
Expressions []compiler.InputExpression
TestSuiteFilePath string
FileDescriptorSetPath string
testSuiteParser TestSuiteParser
}

// Test represents a single test case to be executed. It encompasses the following:
Expand Down Expand Up @@ -336,24 +322,6 @@ func fileDescriptorSet(path string) (*descpb.FileDescriptorSet, error) {
return fds, nil
}

func defaultTestSuiteParserTextproto(t *testing.T, path any) (*conformancepb.TestSuite, error) {
t.Helper()
testSuite := &conformancepb.TestSuite{}
err := loadInput(path.(string), compiler.TextProto, testSuite)
return testSuite, err
}

func defaultTestSuiteParserYAML(t *testing.T, path any) (*test.Suite, error) {
t.Helper()
testSuiteBytes, err := os.ReadFile(path.(string))
if err != nil {
return nil, fmt.Errorf("os.ReadFile(%q) failed: %v", path.(string), err)
}
testSuite := &test.Suite{}
err = yaml.Unmarshal(testSuiteBytes, testSuite)
return testSuite, err
}

// Programs creates a list of CEL programs from the input expressions configured in the test runner
// using the provided program options.
func (tr *TestRunner) Programs(t *testing.T, opts ...cel.ProgramOption) ([]cel.Program, error) {
Expand All @@ -373,6 +341,10 @@ func (tr *TestRunner) Programs(t *testing.T, opts ...cel.ProgramOption) ([]cel.P
}
prg, err := e.Program(ast, opts...)
if err != nil {
if strings.Contains(err.Error(), "invalid file extension") ||
strings.Contains(err.Error(), "invalid raw expression") {
continue
}
return nil, err
}
programs = append(programs, prg)
Expand All @@ -386,24 +358,25 @@ func (tr *TestRunner) Tests(t *testing.T) ([]*Test, error) {
if tr.Compiler == nil {
return nil, fmt.Errorf("compiler is not set")
}
if tr.TestSuiteParserTextproto != nil {
err := registerMessages(tr.FileDescriptorSetPath)
if err != nil {
return nil, fmt.Errorf("registerMessages(%q) failed: %v", tr.FileDescriptorSetPath, err)
}
testSuite, err := tr.TestSuiteParserTextproto(t, tr.TestSuiteFilePath)
if err != nil {
return nil, err
}
return tr.createTestsFromTextproto(t, testSuite)
if tr.testSuiteParser == nil {
return nil, fmt.Errorf("test suite parser is not set")
}
if tr.TestSuiteParserYAML != nil {
testSuite, err := tr.TestSuiteParserYAML(t, tr.TestSuiteFilePath)
if err != nil {
return nil, err
}
if testSuite, err := tr.testSuiteParser.ParseYAML(tr.TestSuiteFilePath); err != nil &&
!strings.Contains(err.Error(), "invalid file extension") {
return nil, fmt.Errorf("tr.testSuiteParser.ParseYAML(%q) failed: %v", tr.TestSuiteFilePath, err)
} else if testSuite != nil {
return tr.createTestsFromYAML(t, testSuite)
}
err := registerMessages(tr.FileDescriptorSetPath)
if err != nil {
return nil, fmt.Errorf("registerMessages(%q) failed: %v", tr.FileDescriptorSetPath, err)
}
if testSuite, err := tr.testSuiteParser.ParseTextproto(tr.TestSuiteFilePath); err != nil &&
!strings.Contains(err.Error(), "invalid file extension") {
return nil, fmt.Errorf("tr.testSuiteParser.ParseTextproto(%q) failed: %v", tr.TestSuiteFilePath, err)
} else if testSuite != nil {
return tr.createTestsFromTextproto(t, testSuite)
}
return nil, nil
}

Expand Down
20 changes: 14 additions & 6 deletions tools/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ func (c *CompiledExpression) CreateAST(_ Compiler) (*cel.Ast, map[string]any, er
var expr exprpb.CheckedExpr
format := InferFileFormat(c.Path)
if format != BinaryProto && format != TextProto {
return nil, nil, fmt.Errorf("file extension must be .binarypb or .textproto: found %v", format)
return nil, nil, fmt.Errorf("invalid file extension wanted: .binarypb or .textproto found: %v", format)
}
if err := loadProtoFile(c.Path, format, &expr); err != nil {
return nil, nil, err
Expand All @@ -483,20 +483,24 @@ func (f *FileExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any,
if err != nil {
return nil, nil, err
}
data, err := loadFile(f.Path)
if err != nil {
return nil, nil, err
}
format := InferFileFormat(f.Path)
switch format {
case CELString:
data, err := loadFile(f.Path)
if err != nil {
return nil, nil, err
}
src := common.NewStringSource(string(data), f.Path)
ast, iss := e.CompileSource(src)
if iss.Err() != nil {
return nil, nil, fmt.Errorf("e.CompileSource(%q) failed: %w", src.Content(), iss.Err())
}
return ast, nil, nil
case CELPolicy, TextYAML:
data, err := loadFile(f.Path)
if err != nil {
return nil, nil, err
}
src := policy.ByteSource(data, f.Path)
parser, err := compiler.CreatePolicyParser()
if err != nil {
Expand All @@ -518,7 +522,7 @@ func (f *FileExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any,
}
return ast, policyMetadata, nil
default:
return nil, nil, fmt.Errorf("unsupported file format: %v", format)
return nil, nil, fmt.Errorf("invalid file extension wanted: .cel or .celpolicy or .yaml found: %v", format)
}
}

Expand All @@ -543,6 +547,10 @@ func (r *RawExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any,
if err != nil {
return nil, nil, err
}
format := InferFileFormat(r.Value)
if format != Unspecified {
return nil, nil, fmt.Errorf("invalid raw expression found file with extension: %v", format)
}
ast, iss := e.Compile(r.Value)
if iss.Err() != nil {
return nil, nil, fmt.Errorf("e.Compile(%q) failed: %w", r.Value, iss.Err())
Expand Down