Skip to content

Commit dda5391

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

16 files changed

Lines changed: 708 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: 25 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,31 @@ 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_code_findbugs_annotations",
71+
"@maven//:com_google_errorprone_error_prone_annotations",
72+
"@maven//:com_google_guava_guava",
73+
],
74+
)
75+
5376
java_library(
5477
name = "test_runner_library",
5578
srcs = ["TestRunnerLibrary.java"],
5679
tags = [
5780
],
5881
deps = [
82+
":cel_coverage_index",
5983
":cel_expression_source",
6084
":cel_test_context",
6185
":cel_test_suite",
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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(nodeId, stats, isInterestingBooleanNode, exprText, logUnencountered, report);
128+
129+
if (isInterestingBooleanNode) {
130+
precedingTabs = updateBooleanBranchCoverage(nodeId, 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+
long nodeId,
151+
NodeCoverageStats stats,
152+
boolean isInterestingBooleanNode,
153+
String exprText,
154+
boolean logUnencountered,
155+
CoverageReport report) {
156+
if (stats.covered) {
157+
report.coveredNodes++;
158+
return logUnencountered;
159+
} else {
160+
if (logUnencountered) {
161+
if (isInterestingBooleanNode) {
162+
report.unencounteredNodes.add(String.format("Expression ID %d ('%s')", nodeId, exprText));
163+
}
164+
// Once an unencountered node is found, we don't log further unencountered nodes in its
165+
// subtree to avoid noise.
166+
return false;
167+
}
168+
return logUnencountered;
169+
}
170+
}
171+
172+
/**
173+
* Updates the coverage report for boolean nodes, including branch coverage.
174+
* Returns the potentially modified `precedingTabs` string.
175+
*/
176+
private String updateBooleanBranchCoverage(
177+
long nodeId,
178+
NodeCoverageStats stats,
179+
String exprText,
180+
String precedingTabs,
181+
boolean logUnencountered,
182+
CoverageReport report) {
183+
report.branches += 2;
184+
if (stats.hasTrueBranch) {
185+
report.coveredBooleanOutcomes++;
186+
} else if (logUnencountered) {
187+
report.unencounteredBranches.add(
188+
String.format(
189+
"%sExpression ID %d ('%s'): Never evaluated to 'true'",
190+
precedingTabs, nodeId, exprText));
191+
precedingTabs += "\t\t";
192+
}
193+
if (stats.hasFalseBranch) {
194+
report.coveredBooleanOutcomes++;
195+
} else if (logUnencountered) {
196+
report.unencounteredBranches.add(
197+
String.format(
198+
"%sExpression ID %d ('%s'): Never evaluated to 'false'",
199+
precedingTabs, nodeId, exprText));
200+
precedingTabs += "\t\t";
201+
}
202+
return precedingTabs;
203+
}
204+
205+
@ThreadSafe
206+
private static final class EvaluationListener implements CelEvaluationListener {
207+
208+
private final Map<Long, NodeCoverageStats> nodeCoverageStatsMap;
209+
private final CelCoverageIndex celCoverageIndex;
210+
211+
EvaluationListener(
212+
CelCoverageIndex celCoverageIndex, Map<Long, NodeCoverageStats> nodeCoverageStatsMap) {
213+
this.celCoverageIndex = celCoverageIndex;
214+
this.nodeCoverageStatsMap = nodeCoverageStatsMap;
215+
}
216+
217+
@Override
218+
public void callback(CelExpr celExpr, Object evaluationResult) {
219+
NodeCoverageStats nodeCoverageStats =
220+
nodeCoverageStatsMap.getOrDefault(celExpr.id(), new NodeCoverageStats());
221+
nodeCoverageStats.covered = true;
222+
nodeCoverageStats.isBooleanNode = celCoverageIndex.inferBooleanNodeType(celExpr);
223+
if (nodeCoverageStats.isBooleanNode) {
224+
if (evaluationResult instanceof Boolean) {
225+
if ((Boolean) evaluationResult) {
226+
nodeCoverageStats.hasTrueBranch = true;
227+
} else {
228+
nodeCoverageStats.hasFalseBranch = true;
229+
}
230+
}
231+
}
232+
nodeCoverageStatsMap.put(celExpr.id(), nodeCoverageStats);
233+
}
234+
}
235+
}

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)