Skip to content

Commit 5be4abc

Browse files
l46kokcopybara-github
authored andcommitted
Track incompatible output types for accurate error reporting
PiperOrigin-RevId: 911470209
1 parent f89d7e5 commit 5be4abc

10 files changed

Lines changed: 142 additions & 34 deletions

File tree

conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ java_library(
1111
srcs = glob(["*.java"]),
1212
deps = [
1313
"//:auto_value",
14+
"//:java_truth",
1415
"//bundle:cel",
1516
"//policy:parser_factory",
17+
"//policy:validation_exception",
1618
"//policy/testing:k8s_test_tag_handler",
1719
"//runtime:function_binding",
1820
"//testing/testrunner:cel_expression_source",

conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414

1515
package dev.cel.conformance.policy;
1616

17+
import static com.google.common.truth.Truth.assertThat;
18+
1719
import com.google.protobuf.Struct;
1820
import dev.cel.bundle.Cel;
1921
import dev.cel.bundle.CelFactory;
2022
import dev.cel.expr.conformance.proto3.TestAllTypes;
2123
import dev.cel.policy.CelPolicyParserFactory;
24+
import dev.cel.policy.CelPolicyValidationException;
2225
import dev.cel.policy.testing.K8sTagHandler;
2326
import dev.cel.runtime.CelFunctionBinding;
2427
import dev.cel.testing.testrunner.CelExpressionSource;
@@ -28,6 +31,7 @@
2831
import java.nio.file.Files;
2932
import java.nio.file.Path;
3033
import java.nio.file.Paths;
34+
import java.util.Locale;
3135
import org.junit.runners.model.Statement;
3236

3337
/** Statement representing a single CEL policy conformance test case. */
@@ -95,6 +99,16 @@ public void evaluate() throws Throwable {
9599
contextBuilder.setConfigFile(textprotoConfigPath.toString());
96100
}
97101

98-
TestRunnerLibrary.runTest(testCase, contextBuilder.build());
102+
try {
103+
TestRunnerLibrary.runTest(testCase, contextBuilder.build());
104+
} catch (CelPolicyValidationException e) {
105+
if (testCase.output().kind() == CelTestCase.Output.Kind.EVAL_ERROR) {
106+
String expectedError = testCase.output().evalError().get(0).toString();
107+
assertThat(e.getMessage().toLowerCase(Locale.US))
108+
.contains(expectedError.toLowerCase(Locale.US));
109+
} else {
110+
throw e;
111+
}
112+
}
99113
}
100114
}

conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class PolicyConformanceTestRunner extends ParentRunner<PolicyConfor
4444
private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings();
4545
private static final String TESTS_YAML_FILE_NAME = "tests.yaml";
4646
private static final String TESTS_TEXTPROTO_FILE_NAME = "tests.textproto";
47+
private static final String POLICY_YAML_FILE_NAME = "policy.yaml";
4748
private static final TypeRegistry TYPE_REGISTRY =
4849
TypeRegistry.newBuilder()
4950
.add(Struct.getDescriptor())
@@ -73,12 +74,40 @@ private static ImmutableList<String> discoverTestDirs(String testdataDir) {
7374
if (!dir.exists() || !dir.isDirectory()) {
7475
return ImmutableList.of();
7576
}
76-
String[] directories = dir.list((current, name) -> new File(current, name).isDirectory());
77-
if (directories == null) {
77+
File[] topLevelDirs = dir.listFiles(File::isDirectory);
78+
if (topLevelDirs == null) {
7879
return ImmutableList.of();
7980
}
80-
Arrays.sort(directories);
81-
return ImmutableList.copyOf(directories);
81+
82+
ImmutableList.Builder<String> testDirsBuilder = ImmutableList.builder();
83+
Arrays.sort(topLevelDirs);
84+
for (File topLevelDir : topLevelDirs) {
85+
if (hasTestSuite(topLevelDir)) {
86+
testDirsBuilder.add(topLevelDir.getName());
87+
continue;
88+
}
89+
90+
// Check one level deeper to support nested tests like compile_errors/unreachable
91+
File[] subDirs = topLevelDir.listFiles(File::isDirectory);
92+
if (subDirs == null) {
93+
continue;
94+
}
95+
96+
Arrays.sort(subDirs);
97+
for (File subDir : subDirs) {
98+
if (hasTestSuite(subDir)) {
99+
testDirsBuilder.add(topLevelDir.getName() + "/" + subDir.getName());
100+
}
101+
}
102+
}
103+
104+
return testDirsBuilder.build();
105+
}
106+
107+
private static boolean hasTestSuite(File dir) {
108+
return (new File(dir, TESTS_YAML_FILE_NAME).exists()
109+
|| new File(dir, TESTS_TEXTPROTO_FILE_NAME).exists())
110+
&& new File(dir, POLICY_YAML_FILE_NAME).exists();
82111
}
83112

84113
private final ImmutableList<PolicyConformanceTest> tests;

policy/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ java_library(
5959
name = "compiler_builder",
6060
exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
6161
)
62+
63+
java_library(
64+
name = "rule_composer",
65+
visibility = ["//:internal"],
66+
exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"],
67+
)

policy/src/main/java/dev/cel/policy/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ java_library(
244244
java_library(
245245
name = "rule_composer",
246246
srcs = ["RuleComposer.java"],
247-
visibility = ["//visibility:private"],
248247
deps = [
249248
":compiled_rule",
250249
"//bundle:cel",
@@ -257,6 +256,8 @@ java_library(
257256
"//common/ast:mutable_expr",
258257
"//common/formats:value_string",
259258
"//common/navigation:mutable_navigation",
259+
"//common/types",
260+
"//common/types:type_providers",
260261
"//extensions:optional_library",
261262
"//optimizer:ast_optimizer",
262263
"//optimizer:mutable_ast",

policy/src/main/java/dev/cel/policy/RuleComposer.java

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import dev.cel.common.formats.ValueString;
3333
import dev.cel.common.navigation.CelNavigableMutableAst;
3434
import dev.cel.common.navigation.CelNavigableMutableExpr;
35+
import dev.cel.common.types.CelType;
36+
import dev.cel.common.types.OptionalType;
3537
import dev.cel.extensions.CelOptionalLibrary.Function;
3638
import dev.cel.optimizer.AstMutator;
3739
import dev.cel.optimizer.CelAstOptimizer;
@@ -67,10 +69,14 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
6769
// If the rule has an optional output, the last result in the ternary should return
6870
// `optional.none`. This output is implicit and created here to reflect the desired
6971
// last possible output of this type of rule.
72+
CelType ruleOutputType = getRuleOutputType(compiledRule);
73+
// The expected output type of the rule, used to verify that all branches agree on the type.
74+
CelType expectedOutputType = null;
7075
if (compiledRule.hasOptionalOutput()) {
7176
output =
7277
Step.newUnconditionalOptionalStep(
7378
newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()));
79+
expectedOutputType = OptionalType.create(ruleOutputType);
7480
}
7581

7682
long lastOutputId = 0;
@@ -79,47 +85,61 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
7985
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
8086
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);
8187

88+
CelType currentType;
89+
Step step;
90+
long currentSourceId = lastOutputId;
91+
8292
switch (match.result().kind()) {
8393
case OUTPUT:
8494
// If the match has an output, then it is considered a non-optional output since
8595
// it is explicitly stated. If the rule itself is optional, then the base case value
8696
// of output being optional.none() will convert the non-optional value to an optional
8797
// one.
8898
OutputValue matchOutput = match.result().output();
89-
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
90-
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
99+
currentType = matchOutput.ast().getResultType();
100+
step =
101+
Step.newNonOptionalStep(
102+
!isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast()));
103+
currentSourceId = matchOutput.sourceId();
104+
105+
CelType baseType = (expectedOutputType == null) ? currentType : expectedOutputType;
106+
String outputFailureMessage =
107+
String.format(
108+
"incompatible output types: block has output type %s, but previous outputs have"
109+
+ " type %s",
110+
baseType.name(), currentType.name());
111+
91112
output = combine(astMutator, step, output);
113+
expectedOutputType = (expectedOutputType == null) ? currentType : expectedOutputType;
92114

93115
assertComposedAstIsValid(
94-
cel,
95-
output.expr,
96-
"conflicting output types found.",
97-
matchOutput.sourceId(),
98-
lastOutputId);
99-
lastOutputId = matchOutput.sourceId();
116+
cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId);
100117
break;
101118
case RULE:
102119
// If the match has a nested rule, then compute the rule and whether it has
103120
// an optional return value.
104121
CelCompiledRule matchNestedRule = match.result().rule();
122+
currentType = getRuleOutputType(matchNestedRule);
105123
Step nestedRule = optimizeRule(cel, matchNestedRule);
106-
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();
124+
step =
125+
new Step(
126+
matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr);
127+
currentSourceId = matchNestedRule.sourceId();
128+
129+
String ruleFailureMessage =
130+
String.format(
131+
"failed composing the subrule '%s' due to incompatible output types.",
132+
matchNestedRule.ruleId().map(ValueString::value).orElse(""));
107133

108-
Step ruleStep =
109-
nestedHasOptional
110-
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
111-
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
112-
output = combine(astMutator, ruleStep, output);
134+
output = combine(astMutator, step, output);
135+
expectedOutputType = (expectedOutputType == null) ? currentType : expectedOutputType;
113136

114137
assertComposedAstIsValid(
115-
cel,
116-
output.expr,
117-
String.format(
118-
"failed composing the subrule '%s' due to conflicting output types.",
119-
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
120-
lastOutputId);
138+
cel, output.expr, ruleFailureMessage, currentSourceId, lastOutputId);
121139
break;
122140
}
141+
142+
lastOutputId = currentSourceId;
123143
}
124144

125145
CelMutableAst resultExpr = output.expr;
@@ -281,6 +301,20 @@ private void assertComposedAstIsValid(
281301
}
282302
}
283303

304+
private CelType getRuleOutputType(CelCompiledRule rule) {
305+
for (CelCompiledMatch match : rule.matches()) {
306+
switch (match.result().kind()) {
307+
case OUTPUT:
308+
return match.result().output().ast().getResultType();
309+
case RULE:
310+
return getRuleOutputType(match.result().rule());
311+
}
312+
throw new IllegalStateException("Unknown match result kind: " + match.result().kind());
313+
}
314+
315+
throw new IllegalStateException("Policy rule contains no outputs");
316+
}
317+
284318
// Step represents an intermediate stage of rule and match expression composition.
285319
//
286320
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
@@ -311,11 +345,6 @@ private Step(
311345
this.expr = expr;
312346
}
313347

314-
private static Step newOptionalStep(
315-
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
316-
return new Step(/* isOptional= */ true, isConditional, cond, expr);
317-
}
318-
319348
private static Step newNonOptionalStep(
320349
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
321350
return new Step(/* isOptional= */ false, isConditional, cond, expr);

policy/src/test/java/dev/cel/policy/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ java_library(
2727
"//parser:parser_factory",
2828
"//parser:unparser",
2929
"//policy",
30+
"//policy:compiled_rule",
3031
"//policy:compiler_factory",
3132
"//policy:parser",
3233
"//policy:parser_factory",
34+
"//policy:rule_composer",
3335
"//policy:source",
3436
"//policy:validation_exception",
3537
"//policy/testing:k8s_test_tag_handler",

policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import dev.cel.bundle.CelEnvironmentYamlParser;
3232
import dev.cel.common.CelAbstractSyntaxTree;
3333
import dev.cel.common.CelOptions;
34+
import dev.cel.common.formats.ValueString;
3435
import dev.cel.common.types.OptionalType;
3536
import dev.cel.common.types.SimpleType;
3637
import dev.cel.expr.conformance.proto3.TestAllTypes;
@@ -519,4 +520,25 @@ private String readExpectedErrorsBaseline() throws IOException {
519520
this.policyFilePath = String.format("%s/policy.yaml", name);
520521
}
521522
}
523+
524+
@Test
525+
public void ruleComposer_ruleWithNoOutputs_throwsIllegalStateException() throws Exception {
526+
Cel cel = newCel();
527+
CelCompiledRule emptyRule =
528+
CelCompiledRule.create(
529+
1L,
530+
Optional.of(ValueString.of(2L, "empty_rule")),
531+
ImmutableList.of(),
532+
ImmutableList.of(),
533+
cel);
534+
535+
RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000);
536+
537+
IllegalStateException e =
538+
assertThrows(
539+
IllegalStateException.class,
540+
() -> composer.optimize(cel.compile("true").getAst(), cel));
541+
542+
assertThat(e).hasMessageThat().isEqualTo("Policy rule contains no outputs");
543+
}
522544
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: conflicting output types found.
1+
ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map, but previous outputs have type bool
22
| output: "false"
33
| .............^
4-
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: conflicting output types found.
4+
ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map, but previous outputs have type bool
55
| - output: "{'banned': true}"
66
| .............^
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to conflicting output types.
1+
ERROR: compose_errors_conflicting_subrule/policy.yaml:22:7: failed composing the subrule 'banned regions' due to incompatible output types.
2+
| id: "banned regions"
3+
| ......^
4+
ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types.
25
| output: "{'banned': false}"
36
| .............^

0 commit comments

Comments
 (0)