Skip to content

Commit 94ed8da

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

File tree

15 files changed

+592
-20
lines changed

15 files changed

+592
-20
lines changed

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: 24 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,10 @@ 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+
],
3741
)
3842

3943
java_library(
@@ -42,6 +46,7 @@ java_library(
4246
tags = [
4347
],
4448
deps = [
49+
":cel_coverage_index",
4550
":cel_expression_source",
4651
":cel_test_context",
4752
":cel_test_suite",
@@ -50,12 +55,30 @@ java_library(
5055
],
5156
)
5257

58+
java_library(
59+
name = "cel_coverage_index",
60+
srcs = ["CelCoverageIndex.java"],
61+
tags = [
62+
],
63+
deps = [
64+
"//common:cel_ast",
65+
"//common/ast",
66+
"//common/navigation",
67+
"//common/types:type_providers",
68+
"//parser:unparser_visitor",
69+
"//runtime:evaluation_listener",
70+
"@maven//:com_google_errorprone_error_prone_annotations",
71+
"@maven//:com_google_guava_guava",
72+
],
73+
)
74+
5375
java_library(
5476
name = "test_runner_library",
5577
srcs = ["TestRunnerLibrary.java"],
5678
tags = [
5779
],
5880
deps = [
81+
":cel_coverage_index",
5982
":cel_expression_source",
6083
":cel_test_context",
6184
":cel_test_suite",
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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+
import java.util.logging.Logger;
32+
33+
/**
34+
* A class for managing the coverage index for CEL tests.
35+
*
36+
* <p>This class is used to manage the coverage index for CEL tests. It provides a method for
37+
* getting the coverage index for a given test case.
38+
*/
39+
final class CelCoverageIndex {
40+
private static final Logger logger = Logger.getLogger(CelCoverageIndex.class.getName());
41+
42+
private CelAbstractSyntaxTree ast;
43+
private final Map<Long, NodeCoverageStats> nodeCoverageStatsMap = new HashMap<>();
44+
45+
public void setAst(CelAbstractSyntaxTree ast) {
46+
this.ast = ast;
47+
CelNavigableExpr.fromExpr(ast.getExpr())
48+
.allNodes()
49+
.forEach(
50+
celNavigableExpr -> {
51+
NodeCoverageStats nodeCoverageStats = new NodeCoverageStats();
52+
nodeCoverageStats.isBooleanNode = inferBooleanNodeType(celNavigableExpr.expr());
53+
nodeCoverageStatsMap.put(celNavigableExpr.id(), nodeCoverageStats);
54+
});
55+
}
56+
57+
/**
58+
* Returns the evaluation listener for the CEL test suite.
59+
*
60+
* <p>This listener is used to track the coverage of the CEL test suite.
61+
*/
62+
public CelEvaluationListener getEvaluationListener() {
63+
return new EvaluationListener(this, nodeCoverageStatsMap);
64+
}
65+
66+
/** Returns the coverage report for the CEL test suite. */
67+
public CoverageReport getCoverageReport() {
68+
CoverageReport report = new CoverageReport();
69+
traverseAndCalculateCoverage(
70+
CelNavigableAst.fromAst(ast).getRoot(), nodeCoverageStatsMap, true, "", report);
71+
report.celExpr = new CelUnparserVisitor(ast).unparse(ast.getExpr());
72+
logger.info("CEL Expression: " + report.celExpr);
73+
logger.info("Nodes: " + report.nodes);
74+
logger.info("Covered Nodes: " + report.coveredNodes);
75+
logger.info("Branches: " + report.branches);
76+
logger.info("Covered Boolean Outcomes: " + report.coveredBooleanOutcomes);
77+
logger.info("Unencountered Nodes: " + report.unencounteredNodes);
78+
logger.info("Unencountered Branches: " + report.unencounteredBranches);
79+
return report;
80+
}
81+
82+
83+
/** A class for managing the coverage report for a CEL test suite. */
84+
public static final class CoverageReport {
85+
String celExpr;
86+
long nodes = 0L;
87+
long coveredNodes = 0L;
88+
long branches = 0L;
89+
long coveredBooleanOutcomes = 0L;
90+
List<String> unencounteredNodes = new ArrayList<>();
91+
List<String> unencounteredBranches = new ArrayList<>();
92+
}
93+
94+
/** A class for managing the coverage stats for a CEL node. */
95+
private static final class NodeCoverageStats {
96+
Boolean isBooleanNode = false;
97+
Boolean covered = false;
98+
Boolean hasTrueBranch = false;
99+
Boolean hasFalseBranch = false;
100+
}
101+
102+
private Boolean inferBooleanNodeType(CelExpr celExpr) {
103+
return ast.getTypeMap().containsKey(celExpr.id())
104+
&& ast.getTypeMap().get(celExpr.id()).kind().equals(CelKind.BOOL);
105+
}
106+
107+
private void traverseAndCalculateCoverage(
108+
CelNavigableExpr node,
109+
Map<Long, NodeCoverageStats> statsMap,
110+
boolean logUnencountered,
111+
String precedingTabs,
112+
CoverageReport report) {
113+
long nodeId = node.id();
114+
NodeCoverageStats stats = statsMap.getOrDefault(nodeId, new NodeCoverageStats());
115+
report.nodes++;
116+
117+
boolean isInterestingBooleanNode = isInterestingBooleanNode(node, stats);
118+
119+
// Only unparse if the node is interesting (boolean node) and we need to log
120+
// unencountered nodes.
121+
String exprText = "";
122+
if (isInterestingBooleanNode && logUnencountered) {
123+
exprText = new CelUnparserVisitor(ast).unparse(node.expr());
124+
}
125+
126+
// Update coverage for the current node and determine if we should continue logging unencountered.
127+
logUnencountered = updateNodeCoverage(stats, isInterestingBooleanNode, exprText, logUnencountered, report);
128+
129+
if (isInterestingBooleanNode) {
130+
precedingTabs = updateBooleanBranchCoverage(stats, exprText, precedingTabs, logUnencountered, report);
131+
}
132+
133+
for (CelNavigableExpr child : node.children().collect(toImmutableList())) {
134+
traverseAndCalculateCoverage(child, statsMap, logUnencountered, precedingTabs, report);
135+
}
136+
}
137+
138+
private boolean isInterestingBooleanNode(CelNavigableExpr node, NodeCoverageStats stats) {
139+
return stats.isBooleanNode
140+
&& !node.expr().getKind().equals(ExprKind.Kind.CONSTANT)
141+
&& !(node.expr().getKind().equals(ExprKind.Kind.CALL)
142+
&& node.expr().call().function().equals("cel.@block"));
143+
}
144+
145+
/**
146+
* Updates the coverage report based on whether the current node was covered.
147+
* Returns true if logging of unencountered nodes should continue for children, false otherwise.
148+
*/
149+
private boolean updateNodeCoverage(
150+
NodeCoverageStats stats,
151+
boolean isInterestingBooleanNode,
152+
String exprText,
153+
boolean logUnencountered,
154+
CoverageReport report) {
155+
if (stats.covered) {
156+
report.coveredNodes++;
157+
return logUnencountered;
158+
} else {
159+
if (logUnencountered) {
160+
if (isInterestingBooleanNode) {
161+
report.unencounteredNodes.add(String.format("Expression %s", exprText));
162+
}
163+
// Once an unencountered node is found, we don't log further unencountered nodes in its
164+
// subtree to avoid noise.
165+
return false;
166+
}
167+
return logUnencountered;
168+
}
169+
}
170+
171+
/**
172+
* Updates the coverage report for boolean nodes, including branch coverage.
173+
* Returns the potentially modified `precedingTabs` string.
174+
*/
175+
private String updateBooleanBranchCoverage(
176+
NodeCoverageStats stats,
177+
String exprText,
178+
String precedingTabs,
179+
boolean logUnencountered,
180+
CoverageReport report) {
181+
report.branches += 2;
182+
if (stats.hasTrueBranch) {
183+
report.coveredBooleanOutcomes++;
184+
} else if (logUnencountered) {
185+
report.unencounteredBranches.add(
186+
String.format("%sExpression %s: Never evaluated to 'true'", precedingTabs, exprText));
187+
precedingTabs += "\t";
188+
}
189+
if (stats.hasFalseBranch) {
190+
report.coveredBooleanOutcomes++;
191+
} else if (logUnencountered) {
192+
report.unencounteredBranches.add(
193+
String.format("%sExpression %s: Never evaluated to 'false'", precedingTabs, exprText));
194+
precedingTabs += "\t";
195+
}
196+
return precedingTabs;
197+
}
198+
199+
@ThreadSafe
200+
private static final class EvaluationListener implements CelEvaluationListener {
201+
202+
private final Map<Long, NodeCoverageStats> nodeCoverageStatsMap;
203+
private final CelCoverageIndex celCoverageIndex;
204+
205+
EvaluationListener(
206+
CelCoverageIndex celCoverageIndex, Map<Long, NodeCoverageStats> nodeCoverageStatsMap) {
207+
this.celCoverageIndex = celCoverageIndex;
208+
this.nodeCoverageStatsMap = nodeCoverageStatsMap;
209+
}
210+
211+
@Override
212+
public void callback(CelExpr celExpr, Object evaluationResult) {
213+
NodeCoverageStats nodeCoverageStats =
214+
nodeCoverageStatsMap.getOrDefault(celExpr.id(), new NodeCoverageStats());
215+
nodeCoverageStats.covered = true;
216+
nodeCoverageStats.isBooleanNode = celCoverageIndex.inferBooleanNodeType(celExpr);
217+
if (nodeCoverageStats.isBooleanNode) {
218+
if (evaluationResult instanceof Boolean) {
219+
if ((Boolean) evaluationResult) {
220+
nodeCoverageStats.hasTrueBranch = true;
221+
} else {
222+
nodeCoverageStats.hasFalseBranch = true;
223+
}
224+
}
225+
}
226+
nodeCoverageStatsMap.put(celExpr.id(), nodeCoverageStats);
227+
}
228+
}
229+
}

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)