1414package dev .cel .testing .testrunner ;
1515
1616import static com .google .common .collect .ImmutableList .toImmutableList ;
17+ import static java .nio .charset .StandardCharsets .UTF_8 ;
1718
1819import com .google .auto .value .AutoValue ;
1920import com .google .common .collect .ImmutableList ;
2728import dev .cel .common .types .CelKind ;
2829import dev .cel .parser .CelUnparserVisitor ;
2930import dev .cel .runtime .CelEvaluationListener ;
31+ import java .io .UnsupportedEncodingException ;
32+ import java .net .URLEncoder ;
3033import java .util .Map ;
3134import java .util .concurrent .ConcurrentHashMap ;
3235import 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}
0 commit comments