Skip to content

Commit b202efc

Browse files
ChinmayMadeshicopybara-github
authored andcommitted
Support for Dot graph via graphviz
PiperOrigin-RevId: 807965747
1 parent da0f0ae commit b202efc

File tree

5 files changed

+168
-25
lines changed

5 files changed

+168
-25
lines changed

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

Lines changed: 124 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package dev.cel.testing.testrunner;
1515

1616
import static com.google.common.collect.ImmutableList.toImmutableList;
17+
import static java.nio.charset.StandardCharsets.UTF_8;
1718

1819
import com.google.auto.value.AutoValue;
1920
import com.google.common.collect.ImmutableList;
@@ -27,6 +28,8 @@
2728
import dev.cel.common.types.CelKind;
2829
import dev.cel.parser.CelUnparserVisitor;
2930
import dev.cel.runtime.CelEvaluationListener;
31+
import java.io.UnsupportedEncodingException;
32+
import java.net.URLEncoder;
3033
import java.util.Map;
3134
import java.util.concurrent.ConcurrentHashMap;
3235
import java.util.concurrent.atomic.AtomicBoolean;
@@ -43,6 +46,13 @@ final class CelCoverageIndex {
4346

4447
private static final Logger logger = Logger.getLogger(CelCoverageIndex.class.getName());
4548

49+
private static final String DIGRAPH_HEADER = "digraph {\n";
50+
private static final String UNCOVERED_NODE_STYLE = "color=\"indianred2\", style=filled";
51+
private static final String PARTIALLY_COVERED_NODE_STYLE = "color=\"lightyellow\","
52+
+ "style=filled";
53+
private static final String COMPLETELY_COVERED_NODE_STYLE = "color=\"lightgreen\","
54+
+ "style=filled";
55+
4656
private CelAbstractSyntaxTree ast;
4757
private final ConcurrentHashMap<Long, NodeCoverageStats> nodeCoverageStatsMap =
4858
new ConcurrentHashMap<>();
@@ -68,24 +78,6 @@ public CelEvaluationListener newEvaluationListener() {
6878
return new EvaluationListener(nodeCoverageStatsMap);
6979
}
7080

71-
/** Returns the coverage report for the CEL test suite. */
72-
public CoverageReport generateCoverageReport() {
73-
CoverageReport.Builder reportBuilder =
74-
CoverageReport.builder().setCelExpression(new CelUnparserVisitor(ast).unparse());
75-
traverseAndCalculateCoverage(
76-
CelNavigableAst.fromAst(ast).getRoot(), nodeCoverageStatsMap, true, "", reportBuilder);
77-
CoverageReport report = reportBuilder.build();
78-
logger.info("CEL Expression: " + report.celExpression());
79-
logger.info("Nodes: " + report.nodes());
80-
logger.info("Covered Nodes: " + report.coveredNodes());
81-
logger.info("Branches: " + report.branches());
82-
logger.info("Covered Boolean Outcomes: " + report.coveredBooleanOutcomes());
83-
logger.info("Unencountered Nodes: \n" + String.join("\n", report.unencounteredNodes()));
84-
logger.info("Unencountered Branches: \n" + String.join("\n",
85-
report.unencounteredBranches()));
86-
return report;
87-
}
88-
8981
/** A class for managing the coverage report for a CEL test suite. */
9082
@AutoValue
9183
public abstract static class CoverageReport {
@@ -103,12 +95,19 @@ public abstract static class CoverageReport {
10395

10496
public abstract ImmutableList<String> unencounteredBranches();
10597

98+
public abstract String dotGraph();
99+
100+
// Currently only supported inside google3.
101+
public abstract String graphUrl();
102+
106103
public static Builder builder() {
107104
return new AutoValue_CelCoverageIndex_CoverageReport.Builder()
108105
.setNodes(0L)
109106
.setCoveredNodes(0L)
110107
.setBranches(0L)
111108
.setCelExpression("")
109+
.setDotGraph("")
110+
.setGraphUrl("")
112111
.setCoveredBooleanOutcomes(0L);
113112
}
114113

@@ -133,6 +132,10 @@ public abstract static class Builder {
133132

134133
public abstract Builder setCoveredBooleanOutcomes(long value);
135134

135+
public abstract Builder setDotGraph(String value);
136+
137+
public abstract Builder setGraphUrl(String value);
138+
136139
public abstract ImmutableList.Builder<String> unencounteredNodesBuilder();
137140

138141
public abstract ImmutableList.Builder<String> unencounteredBranchesBuilder();
@@ -153,6 +156,33 @@ public final Builder addUnencounteredBranches(String value) {
153156
}
154157
}
155158

159+
/** Returns the coverage report for the CEL test suite. */
160+
public CoverageReport generateCoverageReport() {
161+
CoverageReport.Builder reportBuilder =
162+
CoverageReport.builder().setCelExpression(new CelUnparserVisitor(ast).unparse());
163+
StringBuilder dotGraphBuilder = new StringBuilder(DIGRAPH_HEADER);
164+
traverseAndCalculateCoverage(
165+
CelNavigableAst.fromAst(ast).getRoot(),
166+
nodeCoverageStatsMap,
167+
true,
168+
"",
169+
reportBuilder,
170+
dotGraphBuilder);
171+
dotGraphBuilder.append("}");
172+
String dotGraph = dotGraphBuilder.toString();
173+
CoverageReport report = reportBuilder.setDotGraph(dotGraph).build();
174+
logger.info("CEL Expression: " + report.celExpression());
175+
logger.info("Nodes: " + report.nodes());
176+
logger.info("Covered Nodes: " + report.coveredNodes());
177+
logger.info("Branches: " + report.branches());
178+
logger.info("Covered Boolean Outcomes: " + report.coveredBooleanOutcomes());
179+
logger.info("Unencountered Nodes: \n" + String.join("\n", report.unencounteredNodes()));
180+
logger.info("Unencountered Branches: \n" + String.join("\n",
181+
report.unencounteredBranches()));
182+
logger.info("Dot Graph: " + report.dotGraph());
183+
return report;
184+
}
185+
156186
/** A class for managing the coverage stats for a CEL node. */
157187
@ThreadSafe
158188
private static final class NodeCoverageStats {
@@ -172,19 +202,32 @@ private void traverseAndCalculateCoverage(
172202
Map<Long, NodeCoverageStats> statsMap,
173203
boolean logUnencountered,
174204
String precedingTabs,
175-
CoverageReport.Builder reportBuilder) {
205+
CoverageReport.Builder reportBuilder,
206+
StringBuilder dotGraphBuilder) {
176207
long nodeId = node.id();
177208
NodeCoverageStats stats = statsMap.getOrDefault(nodeId, new NodeCoverageStats());
178209
reportBuilder.setNodes(reportBuilder.nodes() + 1);
179210

180211
boolean isInterestingBooleanNode = isInterestingBooleanNode(node, stats);
181212

182-
// Only unparse if the node is interesting (boolean node) and we need to log
183-
// unencountered nodes.
184-
String exprText = "";
185-
if (isInterestingBooleanNode && logUnencountered) {
186-
exprText = new CelUnparserVisitor(ast).unparse(node.expr());
213+
String exprText = new CelUnparserVisitor(ast).unparse(node.expr());
214+
String nodeCoverageStyle = UNCOVERED_NODE_STYLE;
215+
if (stats.covered.get()) {
216+
if (isInterestingBooleanNode) {
217+
if (stats.hasTrueBranch.get() && stats.hasFalseBranch.get()) {
218+
nodeCoverageStyle = COMPLETELY_COVERED_NODE_STYLE;
219+
} else {
220+
nodeCoverageStyle = PARTIALLY_COVERED_NODE_STYLE;
221+
}
222+
} else {
223+
nodeCoverageStyle = COMPLETELY_COVERED_NODE_STYLE;
224+
}
187225
}
226+
String escapedExprText = escapeSpecialCharacters(exprText);
227+
dotGraphBuilder.append(
228+
String.format(
229+
"%d [shape=record, %s, label=\"{<1> exprID: %d | <2> %s} | <3> %s\"];\n",
230+
nodeId, nodeCoverageStyle, nodeId, kindToString(node), escapedExprText));
188231

189232
// Update coverage for the current node and determine if we should continue logging
190233
// unencountered.
@@ -199,7 +242,9 @@ private void traverseAndCalculateCoverage(
199242
}
200243

201244
for (CelNavigableExpr child : node.children().collect(toImmutableList())) {
202-
traverseAndCalculateCoverage(child, statsMap, logUnencountered, precedingTabs, reportBuilder);
245+
dotGraphBuilder.append(String.format("%d -> %d;\n", nodeId, child.id()));
246+
traverseAndCalculateCoverage(
247+
child, statsMap, logUnencountered, precedingTabs, reportBuilder, dotGraphBuilder);
203248
}
204249
}
205250

@@ -293,4 +338,58 @@ public void callback(CelExpr celExpr, Object evaluationResult) {
293338
}
294339
}
295340
}
341+
342+
private String kindToString(CelNavigableExpr node) {
343+
if (node.parent().isPresent()
344+
&& node.parent().get().expr().getKind().equals(ExprKind.Kind.COMPREHENSION)) {
345+
CelExpr.CelComprehension comp = node.parent().get().expr().comprehension();
346+
if (node.id() == comp.iterRange().id()) {
347+
return "IterRange";
348+
}
349+
if (node.id() == comp.accuInit().id()) {
350+
return "AccuInit";
351+
}
352+
if (node.id() == comp.loopCondition().id()) {
353+
return "LoopCondition";
354+
}
355+
if (node.id() == comp.loopStep().id()) {
356+
return "LoopStep";
357+
}
358+
if (node.id() == comp.result().id()) {
359+
return "Result";
360+
}
361+
}
362+
363+
switch (node.getKind()) {
364+
case CALL:
365+
return "Call Node";
366+
case COMPREHENSION:
367+
return "Comprehension Node";
368+
case IDENT:
369+
return "Ident Node";
370+
case LIST:
371+
return "List Node";
372+
case CONSTANT:
373+
return "Literal Node";
374+
case MAP:
375+
return "Map Node";
376+
case SELECT:
377+
return "Select Node";
378+
case STRUCT:
379+
return "Struct Node";
380+
default:
381+
return "Unspecified Node";
382+
}
383+
}
384+
385+
private String escapeSpecialCharacters(String exprText) {
386+
return exprText
387+
.replace("\"", "\\\"")
388+
.replace("\n", "\\n")
389+
.replace("||", " \\| \\| ")
390+
.replace("<", "\\<")
391+
.replace(">", "\\>")
392+
.replace("{", "\\{")
393+
.replace("}", "\\}");
394+
}
296395
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ private void addCoverageAttributes(
233233
XmlConstants.ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS,
234234
String.join("\n", coverageReport.unencounteredBranches()));
235235
}
236+
currentSuite.setAttribute(
237+
XmlConstants.ATTR_CEL_TEST_COVERAGE_GRAPH_URL, coverageReport.graphUrl());
236238
}
237239
}
238240

@@ -284,5 +286,6 @@ private static final class XmlConstants {
284286
static final String ATTR_AST_BRANCH_COVERAGE = "Ast_Branch_Coverage";
285287
static final String ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS =
286288
"Interesting_Unencountered_Branch_Paths";
289+
static final String ATTR_CEL_TEST_COVERAGE_GRAPH_URL = "Cel_Test_Coverage_Graph_URL";
287290
}
288291
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ java_test(
9797
"//:java_truth",
9898
"//bundle:cel",
9999
"//common:cel_ast",
100+
"//common:options",
100101
"//common/types",
102+
"//compiler:compiler_builder",
103+
"//extensions",
104+
"//parser:macro",
101105
"//runtime",
102106
"//runtime:evaluation_listener",
103107
"//testing/testrunner:cel_coverage_index",

testing/src/test/java/dev/cel/testing/testrunner/CelCoverageIndexTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
import dev.cel.bundle.Cel;
2020
import dev.cel.bundle.CelFactory;
2121
import dev.cel.common.CelAbstractSyntaxTree;
22+
import dev.cel.common.CelOptions;
2223
import dev.cel.common.types.SimpleType;
24+
import dev.cel.compiler.CelCompiler;
25+
import dev.cel.extensions.CelExtensions;
26+
import dev.cel.parser.CelStandardMacro;
2327
import dev.cel.runtime.CelEvaluationListener;
2428
import dev.cel.runtime.CelRuntime;
2529
import dev.cel.testing.testrunner.CelCoverageIndex.CoverageReport;
@@ -90,4 +94,35 @@ public void getCoverageReport_partialCoverage_shortCircuit() throws Exception {
9094
"Expression ID 4 ('x > 1 && y > 1'): lacks 'true' coverage",
9195
"\t\tExpression ID 2 ('x > 1'): lacks 'true' coverage");
9296
}
97+
98+
@Test
99+
public void getCoverageReport_comprehension_generatesDotGraph() throws Exception {
100+
cel = CelFactory.standardCelBuilder().build();
101+
CelCompiler compiler =
102+
cel.toCompilerBuilder()
103+
.setOptions(CelOptions.newBuilder().populateMacroCalls(true).build())
104+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
105+
.addLibraries(CelExtensions.comprehensions())
106+
.build();
107+
ast = compiler.compile("[1, 2, 3].all(i, i % 2 != 0)").getAst();
108+
program = cel.createProgram(ast);
109+
CelCoverageIndex coverageIndex = new CelCoverageIndex();
110+
coverageIndex.init(ast);
111+
CelEvaluationListener listener = coverageIndex.newEvaluationListener();
112+
113+
program.trace(ImmutableMap.of(), listener);
114+
115+
CoverageReport report = coverageIndex.generateCoverageReport();
116+
assertThat(report.dotGraph())
117+
.contains("label=\"{<1> exprID: 1 | <2> IterRange} | <3> [1, 2, 3]\"");
118+
assertThat(report.dotGraph()).contains("label=\"{<1> exprID: 12 | <2> AccuInit} | <3> true\"");
119+
assertThat(report.dotGraph()).doesNotContain("red"); // No unencountered nodes.
120+
assertThat(report.dotGraph())
121+
.contains(
122+
"label=\"{<1> exprID: 14 | <2> LoopCondition} | <3>"
123+
+ " @not_strictly_false(@result)\"");
124+
assertThat(report.dotGraph())
125+
.contains("label=\"{<1> exprID: 16 | <2> LoopStep} | <3> @result && i % 2 != 0\"");
126+
assertThat(report.dotGraph()).contains("label=\"{<1> exprID: 17 | <2> Result} | <3> @result\"");
127+
}
93128
}

testing/src/test/java/dev/cel/testing/testrunner/JUnitXmlReporterTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public void testGenerateReport_coverageReport_withCoverage() throws IOException
178178
.addUnencounteredNodes("Node 2")
179179
.addUnencounteredBranches("Branch 1")
180180
.addUnencounteredBranches("Branch 2")
181+
.setGraphUrl("http://graphviz/url")
181182
.build();
182183

183184
when(context.getSuiteName()).thenReturn(SUITE_NAME);
@@ -202,6 +203,7 @@ public void testGenerateReport_coverageReport_withCoverage() throws IOException
202203
+ " name=\"TestSuiteName\" tests=\"1\" time=\"0.9\"><testsuite"
203204
+ " Ast_Branch_Coverage=\"50.00% (5 out of 10 branch outcomes covered)\""
204205
+ " Ast_Node_Coverage=\"100.00% (10 out of 10 nodes covered)\" Cel_Expr=\"\""
206+
+ " Cel_Test_Coverage_Graph_URL=\"http://graphviz/url\""
205207
+ " Interesting_Unencountered_Branch_Paths=\"Branch 1&#10;Branch 2\""
206208
+ " Interesting_Unencountered_Nodes=\"Node 1&#10;Node 2\" errors=\"0\""
207209
+ " failures=\"0\" name=\"TestClass1\" tests=\"1\" time=\"0.4\"><testcase"

0 commit comments

Comments
 (0)