Skip to content

Commit f2aa09e

Browse files
Additional aggregation tests showcasing multiple aggregation rules with nested match behavior
1 parent db79025 commit f2aa09e

8 files changed

Lines changed: 438 additions & 157 deletions

File tree

policy/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ go_test(
7272
"//test:go_default_library",
7373
"//common/debug:go_default_library",
7474
"//common/types:go_default_library",
75-
"//interpreter:go_default_library",
7675
"//common/types/ref:go_default_library",
76+
"//common/types/traits:go_default_library",
77+
"//interpreter:go_default_library",
7778
"//test/proto3pb:go_default_library",
7879
"@in_yaml_go_yaml_v3//:go_default_library",
7980
"@com_github_google_go_cmp//cmp:go_default_library",

policy/compiler_test.go

Lines changed: 33 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -46,52 +46,6 @@ func TestCompile(t *testing.T) {
4646
}
4747
}
4848

49-
func TestRuleComposerError(t *testing.T) {
50-
env, err := cel.NewEnv()
51-
if err != nil {
52-
t.Fatalf("NewEnv() failed: %v", err)
53-
}
54-
_, err = NewRuleComposer(env, ExpressionUnnestHeight(-1))
55-
if err == nil || !strings.Contains(err.Error(), "invalid unnest") {
56-
t.Errorf("NewRuleComposer() got %v, wanted 'invalid unnest'", err)
57-
}
58-
}
59-
60-
func TestRuleComposerUnnest(t *testing.T) {
61-
for _, tst := range composerUnnestTests {
62-
tc := tst
63-
t.Run(tc.name, func(t *testing.T) {
64-
r := newRunner(tc.name, tc.expr, []ParserOption{})
65-
env, rule, iss := r.compileRule(t)
66-
if iss.Err() != nil {
67-
t.Fatalf("CompileRule() failed: %v", iss.Err())
68-
}
69-
rc, err := NewRuleComposer(env, tc.composerOpts...)
70-
if err != nil {
71-
t.Fatalf("NewRuleComposer() failed: %v", err)
72-
}
73-
ast, iss := rc.Compose(rule)
74-
if iss.Err() != nil {
75-
t.Fatalf("Compose(rule) failed: %v", iss.Err())
76-
}
77-
policy := parsePolicy(t, tc.name, []ParserOption{})
78-
verifySourceInfoCoverage(t, policy, ast)
79-
unparsed, err := cel.AstToString(ast)
80-
if err != nil {
81-
t.Fatalf("cel.AstToString() failed: %v", err)
82-
}
83-
if normalize(unparsed) != normalize(tc.composed) {
84-
t.Errorf("cel.AstToString() got %s, wanted %s", unparsed, tc.composed)
85-
}
86-
if !ast.OutputType().IsEquivalentType(tc.outputType) {
87-
t.Errorf("ast.OutputType() got %v, wanted %v", ast.OutputType(), tc.outputType)
88-
}
89-
r.setup(t, env, ast)
90-
r.run(t)
91-
})
92-
}
93-
}
94-
9549
func TestCompileError(t *testing.T) {
9650
for _, tst := range policyErrorTests {
9751
policy := parsePolicy(t, tst.name, []ParserOption{})
@@ -290,6 +244,36 @@ func BenchmarkCompile(b *testing.B) {
290244
}
291245
}
292246

247+
func parsePolicySource(t testing.TB, name string, policySource string, parseOpts ...ParserOption) *Policy {
248+
t.Helper()
249+
p := StringSource(policySource, name)
250+
parser, err := NewParser(parseOpts...)
251+
if err != nil {
252+
t.Fatalf("NewParser() failed: %v", err)
253+
}
254+
policy, iss := parser.Parse(p)
255+
if iss.Err() != nil {
256+
t.Fatalf("parser.Parse() failed: %v", iss.Err())
257+
}
258+
return policy
259+
}
260+
261+
func parseAndCompilePolicy(t testing.TB, name string, policySource string, envOpts []cel.EnvOption, compilerOpts []CompilerOption) (*cel.Env, *cel.Ast, *cel.Issues) {
262+
t.Helper()
263+
policy := parsePolicySource(t, name, policySource)
264+
envOpts = append([]cel.EnvOption{
265+
cel.OptionalTypes(),
266+
cel.EnableMacroCallTracking(),
267+
ext.Bindings(),
268+
}, envOpts...)
269+
env, err := cel.NewEnv(envOpts...)
270+
if err != nil {
271+
t.Fatalf("cel.NewEnv() failed: %v", err)
272+
}
273+
ast, iss := Compile(env, policy, compilerOpts...)
274+
return env, ast, iss
275+
}
276+
293277
func newRunner(name, expr string, parseOpts []ParserOption, opts ...cel.EnvOption) *runner {
294278
return &runner{
295279
name: name,
@@ -772,27 +756,7 @@ rule:
772756
for _, tst := range tests {
773757
tc := tst
774758
t.Run(tc.name, func(t *testing.T) {
775-
p := StringSource(tc.policy, "<input>")
776-
parser, err := NewParser()
777-
if err != nil {
778-
t.Fatalf("NewParser() failed: %v", err)
779-
}
780-
policy, iss := parser.Parse(p)
781-
if iss.Err() != nil {
782-
t.Fatalf("parser.Parse() failed: %v", iss.Err())
783-
}
784-
785-
envOpts := append([]cel.EnvOption{
786-
cel.OptionalTypes(),
787-
cel.EnableMacroCallTracking(),
788-
ext.Bindings(),
789-
}, tc.envOpts...)
790-
env, err := cel.NewEnv(envOpts...)
791-
if err != nil {
792-
t.Fatalf("cel.NewEnv() failed: %v", err)
793-
}
794-
795-
ast, iss := Compile(env, policy)
759+
env, ast, iss := parseAndCompilePolicy(t, tc.name, tc.policy, tc.envOpts, nil)
796760
if tc.wantErr != "" {
797761
if iss.Err() == nil {
798762
t.Fatalf("Compile() succeeded, wanted error %q", tc.wantErr)
@@ -844,15 +808,7 @@ rule:
844808
aggregate:
845809
- condition: 'true'
846810
emit: "'foo'"`
847-
p := StringSource(policySource, "<input>")
848-
parser, err := NewParser()
849-
if err != nil {
850-
t.Fatalf("NewParser() failed: %v", err)
851-
}
852-
policy, iss := parser.Parse(p)
853-
if iss.Err() != nil {
854-
t.Fatalf("parser.Parse() failed: %v", iss.Err())
855-
}
811+
policy := parsePolicySource(t, "aggregate_semantic", policySource)
856812
env, err := cel.NewEnv()
857813
if err != nil {
858814
t.Fatalf("cel.NewEnv() failed: %v", err)
@@ -872,20 +828,7 @@ rule:
872828
aggregate:
873829
- condition: 'false'
874830
emit: "'foo'"`
875-
p := StringSource(policySource, "<input>")
876-
parser, err := NewParser()
877-
if err != nil {
878-
t.Fatalf("NewParser() failed: %v", err)
879-
}
880-
policy, iss := parser.Parse(p)
881-
if iss.Err() != nil {
882-
t.Fatalf("parser.Parse() failed: %v", iss.Err())
883-
}
884-
env, err := cel.NewEnv()
885-
if err != nil {
886-
t.Fatalf("cel.NewEnv() failed: %v", err)
887-
}
888-
_, iss = Compile(env, policy)
831+
_, _, iss := parseAndCompilePolicy(t, "condition_always_false", policySource, nil, nil)
889832
if iss.Err() == nil {
890833
t.Fatalf("Compile() succeeded, wanted error")
891834
}

policy/composer.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *Compiled
195195
matches := r.Matches()
196196
matchCount := len(matches)
197197
var output compositionStep = nil
198-
if !isAggregate && r.HasOptionalOutput() {
198+
// If the rule is non-aggregate and has an optional output, the last result in the ternary should return
199+
// `optional.none`. This output is implicit and created here to reflect the desired
200+
// last possible output of this type of rule.
201+
if !returnList && r.HasOptionalOutput() {
199202
output = newOptionalCompositionStep(ctx, ctx.NewLiteral(types.True), ctx.NewCall("optional.none"))
200203
}
201204
// Build the rule subgraph.
@@ -205,6 +208,10 @@ func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *Compiled
205208

206209
var currentStep compositionStep
207210
if m.Output() != nil {
211+
// If the output is non-nil, then it is considered a non-optional output since
212+
// it is explicitly stated. If the rule itself is optional, then the base case value
213+
// of output being optional.none() will convert the non-optional value to an optional
214+
// one.
208215
out := ctx.CopyASTAndMetadata(m.Output().Expr().NativeRep())
209216
if returnList {
210217
outList := ctx.NewList([]ast.Expr{out}, []int32{})
@@ -213,9 +220,19 @@ func (opt *ruleComposerImpl) optimizeRule(ctx *cel.OptimizerContext, r *Compiled
213220
currentStep = newNonOptionalCompositionStep(ctx, cond, out)
214221
}
215222
} else if m.NestedRule() != nil {
223+
// If the match has a nested rule, then compute the rule and whether it has
224+
// an optional return value.
225+
//
226+
// Semantics for nesting:
227+
// - With optional values (nestedHasOptional = true): The step is treated as optional.
228+
// If the nested rule yields optional.none, composition allows fall-through to
229+
// subsequent match cases.
230+
// - Without optional values (nestedHasOptional = false): The step is treated as non-optional.
231+
// A matching result produces a concrete value that short-circuits further match evaluation,
232+
// though it may be wrapped into optional.of(...) if the outer rule produces optional output.
216233
child := m.NestedRule()
217234
nestedRule := opt.optimizeRule(ctx, child, returnList)
218-
nestedHasOptional := child.HasOptionalOutput()
235+
nestedHasOptional := !returnList && child.HasOptionalOutput()
219236
if nestedHasOptional {
220237
currentStep = newOptionalCompositionStep(ctx, cond, nestedRule)
221238
} else {
@@ -522,12 +539,17 @@ func (s nonOptionalCompositionStep) combine(step compositionStep) compositionSte
522539
if !s.isConditional() {
523540
return s
524541
}
542+
stepExpr := step.expr()
543+
if step.isConditional() {
544+
emptyList := ctx.NewList([]ast.Expr{}, []int32{})
545+
stepExpr = ctx.NewCall(operators.Conditional, step.condition(), step.expr(), emptyList)
546+
}
525547
return newNonOptionalCompositionStep(ctx,
526548
trueCondition,
527549
ctx.NewCall(operators.Conditional,
528550
s.condition(),
529551
s.expr(),
530-
step.expr()))
552+
stepExpr))
531553
}
532554

533555
// newOptionalCompositionStep returns an output step with an optional policy output.
@@ -615,9 +637,7 @@ func isOptionalNone(e ast.Expr) bool {
615637

616638
func removeIneligibleSubExprs(e ast.NavigableExpr, unnestMap map[int64]bool) {
617639
for _, id := range comprehensionSubExprIDs(e) {
618-
if _, found := unnestMap[id]; found {
619-
delete(unnestMap, id)
620-
}
640+
delete(unnestMap, id)
621641
}
622642
}
623643

0 commit comments

Comments
 (0)