Skip to content

Commit f673aa6

Browse files
ChinmayMadeshicopybara-github
authored andcommitted
Setup of the coverage index.
PiperOrigin-RevId: 805684099
1 parent 0bb2f72 commit f673aa6

17 files changed

Lines changed: 773 additions & 20 deletions

File tree

parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public String unparse() {
6666
return stringBuilder.toString();
6767
}
6868

69+
public String unparse(CelExpr expr) {
70+
visit(expr);
71+
return stringBuilder.toString();
72+
}
73+
6974
private static String maybeQuoteField(String field) {
7075
if (RESTRICTED_FIELD_NAMES.contains(field)
7176
|| !IDENTIFIER_SEGMENT_PATTERN.matcher(field).matches()) {

testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ java_library(
1515
],
1616
deps = [
1717
":annotations",
18+
":cel_coverage_index",
1819
":cel_test_suite",
1920
":cel_test_suite_exception",
2021
":cel_test_suite_text_proto_parser",
@@ -33,7 +34,11 @@ java_library(
3334
srcs = ["JUnitXmlReporter.java"],
3435
tags = [
3536
],
36-
deps = ["@maven//:com_google_guava_guava"],
37+
deps = [
38+
":cel_coverage_index",
39+
"@maven//:com_google_guava_guava",
40+
"@maven//:org_jspecify_jspecify",
41+
],
3742
)
3843

3944
java_library(
@@ -42,11 +47,31 @@ java_library(
4247
tags = [
4348
],
4449
deps = [
50+
":cel_coverage_index",
4551
":cel_expression_source",
4652
":cel_test_context",
4753
":cel_test_suite",
4854
":test_runner_library",
4955
"@maven//:junit_junit",
56+
"@maven//:org_jspecify_jspecify",
57+
],
58+
)
59+
60+
java_library(
61+
name = "cel_coverage_index",
62+
srcs = ["CelCoverageIndex.java"],
63+
tags = [
64+
],
65+
deps = [
66+
"//common:cel_ast",
67+
"//common/ast",
68+
"//common/navigation",
69+
"//common/types:type_providers",
70+
"//parser:unparser_visitor",
71+
"//runtime:evaluation_listener",
72+
"@maven//:com_google_code_findbugs_annotations",
73+
"@maven//:com_google_errorprone_error_prone_annotations",
74+
"@maven//:com_google_guava_guava",
5075
],
5176
)
5277

@@ -56,6 +81,7 @@ java_library(
5681
tags = [
5782
],
5883
deps = [
84+
":cel_coverage_index",
5985
":cel_expression_source",
6086
":cel_test_context",
6187
":cel_test_suite",
@@ -80,6 +106,7 @@ java_library(
80106
"@cel_spec//proto/cel/expr:expr_java_proto",
81107
"@maven//:com_google_guava_guava",
82108
"@maven//:com_google_protobuf_protobuf_java",
109+
"@maven//:org_jspecify_jspecify",
83110
],
84111
)
85112

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package dev.cel.testing.testrunner;
15+
16+
import static com.google.common.collect.ImmutableList.toImmutableList;
17+
18+
import javax.annotation.concurrent.ThreadSafe;
19+
import dev.cel.common.CelAbstractSyntaxTree;
20+
import dev.cel.common.ast.CelExpr;
21+
import dev.cel.common.ast.CelExpr.ExprKind;
22+
import dev.cel.common.navigation.CelNavigableAst;
23+
import dev.cel.common.navigation.CelNavigableExpr;
24+
import dev.cel.common.types.CelKind;
25+
import dev.cel.parser.CelUnparserVisitor;
26+
import dev.cel.runtime.CelEvaluationListener;
27+
import java.util.ArrayList;
28+
import java.util.HashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
32+
import java.util.logging.Logger;
33+
34+
/**
35+
* A class for managing the coverage index for CEL tests.
36+
*
37+
* <p>This class is used to manage the coverage index for CEL tests. It provides a method for
38+
* getting the coverage index for a given test case.
39+
*/
40+
final class CelCoverageIndex {
41+
42+
private static final Logger logger = Logger.getLogger(CelCoverageIndex.class.getName());
43+
44+
private CelAbstractSyntaxTree ast;
45+
private final Map<Long, NodeCoverageStats> nodeCoverageStatsMap = new HashMap<>();
46+
47+
public void setAst(CelAbstractSyntaxTree ast) {
48+
this.ast = ast;
49+
CelNavigableExpr.fromExpr(ast.getExpr())
50+
.allNodes()
51+
.forEach(
52+
celNavigableExpr -> {
53+
NodeCoverageStats nodeCoverageStats = new NodeCoverageStats();
54+
nodeCoverageStats.isBooleanNode = inferBooleanNodeType(celNavigableExpr.expr());
55+
nodeCoverageStatsMap.put(celNavigableExpr.id(), nodeCoverageStats);
56+
});
57+
}
58+
59+
/**
60+
* Returns the evaluation listener for the CEL test suite.
61+
*
62+
* <p>This listener is used to track the coverage of the CEL test suite.
63+
*/
64+
public CelEvaluationListener getEvaluationListener() {
65+
return new EvaluationListener(nodeCoverageStatsMap);
66+
}
67+
68+
/** Returns the coverage report for the CEL test suite. */
69+
public CoverageReport getCoverageReport() {
70+
CoverageReport report = new CoverageReport();
71+
traverseAndCalculateCoverage(
72+
CelNavigableAst.fromAst(ast).getRoot(), nodeCoverageStatsMap, true, "", report);
73+
report.celExpr = new CelUnparserVisitor(ast).unparse(ast.getExpr());
74+
logger.info("CEL Expression: " + report.celExpr);
75+
logger.info("Nodes: " + report.nodes);
76+
logger.info("Covered Nodes: " + report.coveredNodes);
77+
logger.info("Branches: " + report.branches);
78+
logger.info("Covered Boolean Outcomes: " + report.coveredBooleanOutcomes);
79+
logger.info("Unencountered Nodes: \n" + String.join("\n", report.unencounteredNodes));
80+
logger.info("Unencountered Branches: \n" + String.join("\n", report.unencounteredBranches));
81+
return report;
82+
}
83+
84+
85+
/** A class for managing the coverage report for a CEL test suite. */
86+
public static final class CoverageReport {
87+
String celExpr;
88+
long nodes = 0L;
89+
long coveredNodes = 0L;
90+
long branches = 0L;
91+
long coveredBooleanOutcomes = 0L;
92+
List<String> unencounteredNodes = new ArrayList<>();
93+
List<String> unencounteredBranches = new ArrayList<>();
94+
}
95+
96+
/** A class for managing the coverage stats for a CEL node. */
97+
private static final class NodeCoverageStats {
98+
Boolean isBooleanNode;
99+
Boolean covered = false;
100+
Boolean hasTrueBranch = false;
101+
Boolean hasFalseBranch = false;
102+
}
103+
104+
private Boolean inferBooleanNodeType(CelExpr celExpr) {
105+
return ast.getTypeMap().containsKey(celExpr.id())
106+
&& ast.getTypeMap().get(celExpr.id()).kind().equals(CelKind.BOOL);
107+
}
108+
109+
private void traverseAndCalculateCoverage(
110+
CelNavigableExpr node,
111+
Map<Long, NodeCoverageStats> statsMap,
112+
boolean logUnencountered,
113+
String precedingTabs,
114+
CoverageReport report) {
115+
long nodeId = node.id();
116+
NodeCoverageStats stats = statsMap.getOrDefault(nodeId, new NodeCoverageStats());
117+
report.nodes++;
118+
119+
boolean isInterestingBooleanNode = isInterestingBooleanNode(node, stats);
120+
121+
// Only unparse if the node is interesting (boolean node) and we need to log
122+
// unencountered nodes.
123+
String exprText = "";
124+
if (isInterestingBooleanNode && logUnencountered) {
125+
exprText = new CelUnparserVisitor(ast).unparse(node.expr());
126+
}
127+
128+
// Update coverage for the current node and determine if we should continue logging unencountered.
129+
logUnencountered = updateNodeCoverage(nodeId, stats, isInterestingBooleanNode, exprText, logUnencountered, report);
130+
131+
if (isInterestingBooleanNode) {
132+
precedingTabs = updateBooleanBranchCoverage(nodeId, stats, exprText, precedingTabs, logUnencountered, report);
133+
}
134+
135+
for (CelNavigableExpr child : node.children().collect(toImmutableList())) {
136+
traverseAndCalculateCoverage(child, statsMap, logUnencountered, precedingTabs, report);
137+
}
138+
}
139+
140+
private boolean isInterestingBooleanNode(CelNavigableExpr node, NodeCoverageStats stats) {
141+
return stats.isBooleanNode
142+
&& !node.expr().getKind().equals(ExprKind.Kind.CONSTANT)
143+
&& !(node.expr().getKind().equals(ExprKind.Kind.CALL)
144+
&& node.expr().call().function().equals("cel.@block"));
145+
}
146+
147+
/**
148+
* Updates the coverage report based on whether the current node was covered.
149+
* Returns true if logging of unencountered nodes should continue for children, false otherwise.
150+
*/
151+
private boolean updateNodeCoverage(
152+
long nodeId,
153+
NodeCoverageStats stats,
154+
boolean isInterestingBooleanNode,
155+
String exprText,
156+
boolean logUnencountered,
157+
CoverageReport report) {
158+
if (stats.covered) {
159+
report.coveredNodes++;
160+
return logUnencountered;
161+
} else {
162+
if (logUnencountered) {
163+
if (isInterestingBooleanNode) {
164+
report.unencounteredNodes.add(String.format("Expression ID %d ('%s')", nodeId, exprText));
165+
}
166+
// Once an unencountered node is found, we don't log further unencountered nodes in its
167+
// subtree to avoid noise.
168+
return false;
169+
}
170+
return logUnencountered;
171+
}
172+
}
173+
174+
/**
175+
* Updates the coverage report for boolean nodes, including branch coverage.
176+
* Returns the potentially modified `precedingTabs` string.
177+
*/
178+
private String updateBooleanBranchCoverage(
179+
long nodeId,
180+
NodeCoverageStats stats,
181+
String exprText,
182+
String precedingTabs,
183+
boolean logUnencountered,
184+
CoverageReport report) {
185+
report.branches += 2;
186+
if (stats.hasTrueBranch) {
187+
report.coveredBooleanOutcomes++;
188+
} else if (logUnencountered) {
189+
report.unencounteredBranches.add(
190+
String.format(
191+
"%sExpression ID %d ('%s'): Never evaluated to 'true'",
192+
precedingTabs, nodeId, exprText));
193+
precedingTabs += "\t\t";
194+
}
195+
if (stats.hasFalseBranch) {
196+
report.coveredBooleanOutcomes++;
197+
} else if (logUnencountered) {
198+
report.unencounteredBranches.add(
199+
String.format(
200+
"%sExpression ID %d ('%s'): Never evaluated to 'false'",
201+
precedingTabs, nodeId, exprText));
202+
precedingTabs += "\t\t";
203+
}
204+
return precedingTabs;
205+
}
206+
207+
@ThreadSafe
208+
private static final class EvaluationListener implements CelEvaluationListener {
209+
210+
private final Map<Long, NodeCoverageStats> nodeCoverageStatsMap;
211+
212+
EvaluationListener(Map<Long, NodeCoverageStats> nodeCoverageStatsMap) {
213+
this.nodeCoverageStatsMap = nodeCoverageStatsMap;
214+
}
215+
216+
@Override
217+
public void callback(CelExpr celExpr, Object evaluationResult) {
218+
NodeCoverageStats nodeCoverageStats = nodeCoverageStatsMap.get(celExpr.id());
219+
nodeCoverageStats.covered = true;
220+
if (nodeCoverageStats.isBooleanNode) {
221+
if (evaluationResult instanceof Boolean) {
222+
if ((Boolean) evaluationResult) {
223+
nodeCoverageStats.hasTrueBranch = true;
224+
} else {
225+
nodeCoverageStats.hasFalseBranch = true;
226+
}
227+
}
228+
}
229+
}
230+
}
231+
}

testing/src/main/java/dev/cel/testing/testrunner/CelUserTestTemplate.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package dev.cel.testing.testrunner;
1616

1717
import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase;
18+
import org.jspecify.annotations.Nullable;
1819
import org.junit.Test;
1920
import org.junit.runner.RunWith;
2021
import org.junit.runners.Parameterized;
@@ -27,7 +28,12 @@
2728
@RunWith(Parameterized.class)
2829
public abstract class CelUserTestTemplate {
2930

30-
@Parameter public CelTestCase testCase;
31+
@Parameter(0)
32+
public CelTestCase testCase;
33+
34+
@Parameter(1)
35+
public @Nullable CelCoverageIndex celCoverageIndex;
36+
3137
private final CelTestContext celTestContext;
3238

3339
public CelUserTestTemplate(CelTestContext celTestContext) {
@@ -36,7 +42,11 @@ public CelUserTestTemplate(CelTestContext celTestContext) {
3642

3743
@Test
3844
public void test() throws Exception {
39-
TestRunnerLibrary.runTest(testCase, updateCelTestContext(celTestContext));
45+
if (celCoverageIndex != null) {
46+
TestRunnerLibrary.runTest(testCase, updateCelTestContext(celTestContext), celCoverageIndex);
47+
} else {
48+
TestRunnerLibrary.runTest(testCase, updateCelTestContext(celTestContext));
49+
}
4050
}
4151

4252
/**

0 commit comments

Comments
 (0)