14
14
package dev .cel .testing .testrunner ;
15
15
16
16
import static com .google .common .collect .ImmutableList .toImmutableList ;
17
+ import static java .nio .charset .StandardCharsets .UTF_8 ;
17
18
18
19
import com .google .auto .value .AutoValue ;
19
20
import com .google .common .collect .ImmutableList ;
27
28
import dev .cel .common .types .CelKind ;
28
29
import dev .cel .parser .CelUnparserVisitor ;
29
30
import dev .cel .runtime .CelEvaluationListener ;
31
+ import java .io .UnsupportedEncodingException ;
32
+ import java .net .URLEncoder ;
30
33
import java .util .Map ;
31
34
import java .util .concurrent .ConcurrentHashMap ;
32
35
import java .util .concurrent .atomic .AtomicBoolean ;
@@ -43,6 +46,13 @@ final class CelCoverageIndex {
43
46
44
47
private static final Logger logger = Logger .getLogger (CelCoverageIndex .class .getName ());
45
48
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
+
46
56
private CelAbstractSyntaxTree ast ;
47
57
private final ConcurrentHashMap <Long , NodeCoverageStats > nodeCoverageStatsMap =
48
58
new ConcurrentHashMap <>();
@@ -68,24 +78,6 @@ public CelEvaluationListener newEvaluationListener() {
68
78
return new EvaluationListener (nodeCoverageStatsMap );
69
79
}
70
80
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
-
89
81
/** A class for managing the coverage report for a CEL test suite. */
90
82
@ AutoValue
91
83
public abstract static class CoverageReport {
@@ -103,12 +95,19 @@ public abstract static class CoverageReport {
103
95
104
96
public abstract ImmutableList <String > unencounteredBranches ();
105
97
98
+ public abstract String dotGraph ();
99
+
100
+ // Currently only supported inside google3.
101
+ public abstract String graphUrl ();
102
+
106
103
public static Builder builder () {
107
104
return new AutoValue_CelCoverageIndex_CoverageReport .Builder ()
108
105
.setNodes (0L )
109
106
.setCoveredNodes (0L )
110
107
.setBranches (0L )
111
108
.setCelExpression ("" )
109
+ .setDotGraph ("" )
110
+ .setGraphUrl ("" )
112
111
.setCoveredBooleanOutcomes (0L );
113
112
}
114
113
@@ -133,6 +132,10 @@ public abstract static class Builder {
133
132
134
133
public abstract Builder setCoveredBooleanOutcomes (long value );
135
134
135
+ public abstract Builder setDotGraph (String value );
136
+
137
+ public abstract Builder setGraphUrl (String value );
138
+
136
139
public abstract ImmutableList .Builder <String > unencounteredNodesBuilder ();
137
140
138
141
public abstract ImmutableList .Builder <String > unencounteredBranchesBuilder ();
@@ -153,6 +156,33 @@ public final Builder addUnencounteredBranches(String value) {
153
156
}
154
157
}
155
158
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
+
156
186
/** A class for managing the coverage stats for a CEL node. */
157
187
@ ThreadSafe
158
188
private static final class NodeCoverageStats {
@@ -172,19 +202,32 @@ private void traverseAndCalculateCoverage(
172
202
Map <Long , NodeCoverageStats > statsMap ,
173
203
boolean logUnencountered ,
174
204
String precedingTabs ,
175
- CoverageReport .Builder reportBuilder ) {
205
+ CoverageReport .Builder reportBuilder ,
206
+ StringBuilder dotGraphBuilder ) {
176
207
long nodeId = node .id ();
177
208
NodeCoverageStats stats = statsMap .getOrDefault (nodeId , new NodeCoverageStats ());
178
209
reportBuilder .setNodes (reportBuilder .nodes () + 1 );
179
210
180
211
boolean isInterestingBooleanNode = isInterestingBooleanNode (node , stats );
181
212
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
+ }
187
225
}
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 ));
188
231
189
232
// Update coverage for the current node and determine if we should continue logging
190
233
// unencountered.
@@ -199,7 +242,9 @@ private void traverseAndCalculateCoverage(
199
242
}
200
243
201
244
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 );
203
248
}
204
249
}
205
250
@@ -293,4 +338,58 @@ public void callback(CelExpr celExpr, Object evaluationResult) {
293
338
}
294
339
}
295
340
}
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
+ }
296
395
}
0 commit comments