Skip to content

Commit 90607e1

Browse files
committed
Add an LCOV coverage report generator for Starlark
The generated coverage report includes function, line, and branch coverage information in LCOV format.
1 parent fc580d7 commit 90607e1

File tree

6 files changed

+718
-5
lines changed

6 files changed

+718
-5
lines changed

src/main/java/net/starlark/java/eval/Eval.java

+36-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import net.starlark.java.syntax.CallExpression;
3030
import net.starlark.java.syntax.Comprehension;
3131
import net.starlark.java.syntax.ConditionalExpression;
32+
import net.starlark.java.syntax.CoverageRecorder;
3233
import net.starlark.java.syntax.DefStatement;
3334
import net.starlark.java.syntax.DictExpression;
3435
import net.starlark.java.syntax.DotExpression;
@@ -45,6 +46,7 @@
4546
import net.starlark.java.syntax.ListExpression;
4647
import net.starlark.java.syntax.LoadStatement;
4748
import net.starlark.java.syntax.Location;
49+
import net.starlark.java.syntax.Parameter;
4850
import net.starlark.java.syntax.Resolver;
4951
import net.starlark.java.syntax.ReturnStatement;
5052
import net.starlark.java.syntax.SliceExpression;
@@ -131,6 +133,7 @@ private static TokenKind execFor(StarlarkThread.Frame fr, ForStatement node)
131133
continue;
132134
case BREAK:
133135
// Finish loop, execute next statement after loop.
136+
CoverageRecorder.getInstance().recordVirtualJump(node);
134137
return TokenKind.PASS;
135138
case RETURN:
136139
// Finish loop, return from function.
@@ -145,6 +148,7 @@ private static TokenKind execFor(StarlarkThread.Frame fr, ForStatement node)
145148
} finally {
146149
EvalUtils.removeIterator(seq);
147150
}
151+
CoverageRecorder.getInstance().recordVirtualJump(node);
148152
return TokenKind.PASS;
149153
}
150154

@@ -159,7 +163,9 @@ private static StarlarkFunction newFunction(StarlarkThread.Frame fr, Resolver.Fu
159163
int nparams =
160164
rfn.getParameters().size() - (rfn.hasKwargs() ? 1 : 0) - (rfn.hasVarargs() ? 1 : 0);
161165
for (int i = 0; i < nparams; i++) {
162-
Expression expr = rfn.getParameters().get(i).getDefaultValue();
166+
Parameter parameter = rfn.getParameters().get(i);
167+
CoverageRecorder.getInstance().recordCoverage(parameter.getIdentifier());
168+
Expression expr = parameter.getDefaultValue();
163169
if (expr == null && defaults == null) {
164170
continue; // skip prefix of required parameters
165171
}
@@ -169,6 +175,10 @@ private static StarlarkFunction newFunction(StarlarkThread.Frame fr, Resolver.Fu
169175
defaults[i - (nparams - defaults.length)] =
170176
expr == null ? StarlarkFunction.MANDATORY : eval(fr, expr);
171177
}
178+
// Visit kwargs and varargs for coverage.
179+
for (int i = nparams; i < rfn.getParameters().size(); i++) {
180+
CoverageRecorder.getInstance().recordCoverage(rfn.getParameters().get(i).getIdentifier());
181+
}
172182
if (defaults == null) {
173183
defaults = EMPTY;
174184
}
@@ -206,6 +216,7 @@ private static TokenKind execIf(StarlarkThread.Frame fr, IfStatement node)
206216
} else if (node.getElseBlock() != null) {
207217
return execStatements(fr, node.getElseBlock(), /*indented=*/ true);
208218
}
219+
CoverageRecorder.getInstance().recordVirtualJump(node);
209220
return TokenKind.PASS;
210221
}
211222

@@ -262,6 +273,7 @@ private static TokenKind exec(StarlarkThread.Frame fr, Statement st)
262273
if (++fr.thread.steps >= fr.thread.stepLimit) {
263274
throw new EvalException("Starlark computation cancelled: too many steps");
264275
}
276+
CoverageRecorder.getInstance().recordCoverage(st);
265277

266278
switch (st.kind()) {
267279
case ASSIGNMENT:
@@ -330,6 +342,7 @@ private static void assign(StarlarkThread.Frame fr, Expression lhs, Object value
330342

331343
private static void assignIdentifier(StarlarkThread.Frame fr, Identifier id, Object value)
332344
throws EvalException {
345+
CoverageRecorder.getInstance().recordCoverage(id);
333346
Resolver.Binding bind = id.getBinding();
334347
switch (bind.getScope()) {
335348
case LOCAL:
@@ -477,6 +490,7 @@ private static Object eval(StarlarkThread.Frame fr, Expression expr)
477490
if (++fr.thread.steps >= fr.thread.stepLimit) {
478491
throw new EvalException("Starlark computation cancelled: too many steps");
479492
}
493+
CoverageRecorder.getInstance().recordCoverage(expr);
480494

481495
// The switch cases have been split into separate functions
482496
// to reduce the stack usage during recursion, which is
@@ -533,9 +547,17 @@ private static Object evalBinaryOperator(StarlarkThread.Frame fr, BinaryOperator
533547
// AND and OR require short-circuit evaluation.
534548
switch (binop.getOperator()) {
535549
case AND:
536-
return Starlark.truth(x) ? eval(fr, binop.getY()) : x;
550+
if (Starlark.truth(x)) {
551+
return eval(fr, binop.getY());
552+
}
553+
CoverageRecorder.getInstance().recordVirtualJump(binop);
554+
return x;
537555
case OR:
538-
return Starlark.truth(x) ? x : eval(fr, binop.getY());
556+
if (Starlark.truth(x)) {
557+
CoverageRecorder.getInstance().recordVirtualJump(binop);
558+
return x;
559+
}
560+
return eval(fr, binop.getY());
539561
default:
540562
Object y = eval(fr, binop.getY());
541563
try {
@@ -577,7 +599,9 @@ private static Object evalDict(StarlarkThread.Frame fr, DictExpression dictexpr)
577599
private static Object evalDot(StarlarkThread.Frame fr, DotExpression dot)
578600
throws EvalException, InterruptedException {
579601
Object object = eval(fr, dot.getObject());
580-
String name = dot.getField().getName();
602+
Identifier field = dot.getField();
603+
CoverageRecorder.getInstance().recordCoverage(field);
604+
String name = field.getName();
581605
try {
582606
return Starlark.getattr(
583607
fr.thread.mutability(), fr.thread.getSemantics(), object, name, /*defaultValue=*/ null);
@@ -627,6 +651,7 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call)
627651
Object[] positional = npos == 0 ? EMPTY : new Object[npos];
628652
for (i = 0; i < npos; i++) {
629653
Argument arg = arguments.get(i);
654+
CoverageRecorder.getInstance().recordCoverage(arg);
630655
Object value = eval(fr, arg.getValue());
631656
positional[i] = value;
632657
}
@@ -635,13 +660,15 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call)
635660
Object[] named = n == npos ? EMPTY : new Object[2 * (n - npos)];
636661
for (int j = 0; i < n; i++) {
637662
Argument.Keyword arg = (Argument.Keyword) arguments.get(i);
663+
CoverageRecorder.getInstance().recordCoverage(arg);
638664
Object value = eval(fr, arg.getValue());
639665
named[j++] = arg.getName();
640666
named[j++] = value;
641667
}
642668

643669
// f(*args) -- varargs
644670
if (star != null) {
671+
CoverageRecorder.getInstance().recordCoverage(star);
645672
Object value = eval(fr, star.getValue());
646673
if (!(value instanceof StarlarkIterable)) {
647674
fr.setErrorLocation(star.getStartLocation());
@@ -656,6 +683,7 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call)
656683

657684
// f(**kwargs)
658685
if (starstar != null) {
686+
CoverageRecorder.getInstance().recordCoverage(starstar);
659687
Object value = eval(fr, starstar.getValue());
660688
if (!(value instanceof Dict)) {
661689
fr.setErrorLocation(starstar.getStartLocation());
@@ -791,6 +819,7 @@ void execClauses(int index) throws EvalException, InterruptedException {
791819
assign(fr, forClause.getVars(), elem);
792820
execClauses(index + 1);
793821
}
822+
CoverageRecorder.getInstance().recordVirtualJump(clause);
794823
} catch (EvalException ex) {
795824
fr.setErrorLocation(forClause.getStartLocation());
796825
throw ex;
@@ -802,12 +831,15 @@ void execClauses(int index) throws EvalException, InterruptedException {
802831
Comprehension.If ifClause = (Comprehension.If) clause;
803832
if (Starlark.truth(eval(fr, ifClause.getCondition()))) {
804833
execClauses(index + 1);
834+
} else {
835+
CoverageRecorder.getInstance().recordVirtualJump(clause);
805836
}
806837
}
807838
return;
808839
}
809840

810841
// base case: evaluate body and add to result.
842+
CoverageRecorder.getInstance().recordCoverage(comp.getBody());
811843
if (dict != null) {
812844
DictExpression.Entry body = (DictExpression.Entry) comp.getBody();
813845
Object k = eval(fr, body.getKey());

src/main/java/net/starlark/java/eval/Starlark.java

+12
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,23 @@
2323
import com.google.errorprone.annotations.FormatMethod;
2424
import java.io.IOException;
2525
import java.io.OutputStream;
26+
import java.io.PrintWriter;
2627
import java.lang.reflect.Method;
2728
import java.math.BigInteger;
2829
import java.time.Duration;
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.Set;
3233
import java.util.TreeSet;
34+
import java.util.function.Function;
35+
import java.util.regex.Pattern;
3336
import javax.annotation.Nullable;
3437
import javax.annotation.concurrent.Immutable;
3538
import net.starlark.java.annot.StarlarkAnnotations;
3639
import net.starlark.java.annot.StarlarkBuiltin;
3740
import net.starlark.java.annot.StarlarkMethod;
3841
import net.starlark.java.spelling.SpellChecker;
42+
import net.starlark.java.syntax.CoverageRecorder;
3943
import net.starlark.java.syntax.Expression;
4044
import net.starlark.java.syntax.FileOptions;
4145
import net.starlark.java.syntax.ParserInput;
@@ -970,4 +974,12 @@ public static boolean startCpuProfile(OutputStream out, Duration period) {
970974
public static void stopCpuProfile() throws IOException {
971975
CpuProfiler.stop();
972976
}
977+
978+
public static void startCoverageCollection(Function<String, Boolean> filenameMatcher) {
979+
CoverageRecorder.startCoverageCollection(filenameMatcher);
980+
}
981+
982+
public static void dumpCoverage(PrintWriter out) {
983+
CoverageRecorder.getInstance().dump(out);
984+
}
973985
}

src/main/java/net/starlark/java/syntax/BUILD

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ java_library(
2121
"Comment.java",
2222
"Comprehension.java",
2323
"ConditionalExpression.java",
24+
"CoverageRecorder.java",
25+
"CoverageVisitor.java",
2426
"DefStatement.java",
2527
"DictExpression.java",
2628
"DotExpression.java",

0 commit comments

Comments
 (0)