-
Notifications
You must be signed in to change notification settings - Fork 0
Implement coverage reports for Starlark #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
edc799e
4d1fe9b
fc580d7
90607e1
b603de0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ | |
import net.starlark.java.syntax.CallExpression; | ||
import net.starlark.java.syntax.Comprehension; | ||
import net.starlark.java.syntax.ConditionalExpression; | ||
import net.starlark.java.syntax.CoverageRecorder; | ||
import net.starlark.java.syntax.DefStatement; | ||
import net.starlark.java.syntax.DictExpression; | ||
import net.starlark.java.syntax.DotExpression; | ||
|
@@ -45,6 +46,7 @@ | |
import net.starlark.java.syntax.ListExpression; | ||
import net.starlark.java.syntax.LoadStatement; | ||
import net.starlark.java.syntax.Location; | ||
import net.starlark.java.syntax.Parameter; | ||
import net.starlark.java.syntax.Resolver; | ||
import net.starlark.java.syntax.ReturnStatement; | ||
import net.starlark.java.syntax.SliceExpression; | ||
|
@@ -131,6 +133,7 @@ private static TokenKind execFor(StarlarkThread.Frame fr, ForStatement node) | |
continue; | ||
case BREAK: | ||
// Finish loop, execute next statement after loop. | ||
CoverageRecorder.getInstance().recordVirtualJump(node); | ||
return TokenKind.PASS; | ||
case RETURN: | ||
// Finish loop, return from function. | ||
|
@@ -145,6 +148,7 @@ private static TokenKind execFor(StarlarkThread.Frame fr, ForStatement node) | |
} finally { | ||
EvalUtils.removeIterator(seq); | ||
} | ||
CoverageRecorder.getInstance().recordVirtualJump(node); | ||
return TokenKind.PASS; | ||
} | ||
|
||
|
@@ -159,7 +163,9 @@ private static StarlarkFunction newFunction(StarlarkThread.Frame fr, Resolver.Fu | |
int nparams = | ||
rfn.getParameters().size() - (rfn.hasKwargs() ? 1 : 0) - (rfn.hasVarargs() ? 1 : 0); | ||
for (int i = 0; i < nparams; i++) { | ||
Expression expr = rfn.getParameters().get(i).getDefaultValue(); | ||
Parameter parameter = rfn.getParameters().get(i); | ||
CoverageRecorder.getInstance().recordCoverage(parameter.getIdentifier()); | ||
Expression expr = parameter.getDefaultValue(); | ||
Comment on lines
+166
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Null‑safety for anonymous * / ** parameters
- CoverageRecorder.getInstance().recordCoverage(parameter.getIdentifier());
+ Identifier pid = parameter.getIdentifier();
+ if (pid != null) {
+ CoverageRecorder.getInstance().recordCoverage(pid);
+ } Apply the same guard inside the later loop that visits varargs/kwargs. Also applies to: 178-181 |
||
if (expr == null && defaults == null) { | ||
continue; // skip prefix of required parameters | ||
} | ||
|
@@ -169,6 +175,10 @@ private static StarlarkFunction newFunction(StarlarkThread.Frame fr, Resolver.Fu | |
defaults[i - (nparams - defaults.length)] = | ||
expr == null ? StarlarkFunction.MANDATORY : eval(fr, expr); | ||
} | ||
// Visit kwargs and varargs for coverage. | ||
for (int i = nparams; i < rfn.getParameters().size(); i++) { | ||
CoverageRecorder.getInstance().recordCoverage(rfn.getParameters().get(i).getIdentifier()); | ||
} | ||
if (defaults == null) { | ||
defaults = EMPTY; | ||
} | ||
|
@@ -206,6 +216,7 @@ private static TokenKind execIf(StarlarkThread.Frame fr, IfStatement node) | |
} else if (node.getElseBlock() != null) { | ||
return execStatements(fr, node.getElseBlock(), /*indented=*/ true); | ||
} | ||
CoverageRecorder.getInstance().recordVirtualJump(node); | ||
return TokenKind.PASS; | ||
} | ||
|
||
|
@@ -262,6 +273,7 @@ private static TokenKind exec(StarlarkThread.Frame fr, Statement st) | |
if (++fr.thread.steps >= fr.thread.stepLimit) { | ||
throw new EvalException("Starlark computation cancelled: too many steps"); | ||
} | ||
CoverageRecorder.getInstance().recordCoverage(st); | ||
|
||
switch (st.kind()) { | ||
case ASSIGNMENT: | ||
|
@@ -330,6 +342,7 @@ private static void assign(StarlarkThread.Frame fr, Expression lhs, Object value | |
|
||
private static void assignIdentifier(StarlarkThread.Frame fr, Identifier id, Object value) | ||
throws EvalException { | ||
CoverageRecorder.getInstance().recordCoverage(id); | ||
Resolver.Binding bind = id.getBinding(); | ||
switch (bind.getScope()) { | ||
case LOCAL: | ||
|
@@ -477,6 +490,7 @@ private static Object eval(StarlarkThread.Frame fr, Expression expr) | |
if (++fr.thread.steps >= fr.thread.stepLimit) { | ||
throw new EvalException("Starlark computation cancelled: too many steps"); | ||
} | ||
CoverageRecorder.getInstance().recordCoverage(expr); | ||
|
||
// The switch cases have been split into separate functions | ||
// to reduce the stack usage during recursion, which is | ||
|
@@ -533,9 +547,17 @@ private static Object evalBinaryOperator(StarlarkThread.Frame fr, BinaryOperator | |
// AND and OR require short-circuit evaluation. | ||
switch (binop.getOperator()) { | ||
case AND: | ||
return Starlark.truth(x) ? eval(fr, binop.getY()) : x; | ||
if (Starlark.truth(x)) { | ||
return eval(fr, binop.getY()); | ||
} | ||
CoverageRecorder.getInstance().recordVirtualJump(binop); | ||
return x; | ||
case OR: | ||
return Starlark.truth(x) ? x : eval(fr, binop.getY()); | ||
if (Starlark.truth(x)) { | ||
CoverageRecorder.getInstance().recordVirtualJump(binop); | ||
return x; | ||
} | ||
return eval(fr, binop.getY()); | ||
default: | ||
Object y = eval(fr, binop.getY()); | ||
try { | ||
|
@@ -577,7 +599,9 @@ private static Object evalDict(StarlarkThread.Frame fr, DictExpression dictexpr) | |
private static Object evalDot(StarlarkThread.Frame fr, DotExpression dot) | ||
throws EvalException, InterruptedException { | ||
Object object = eval(fr, dot.getObject()); | ||
String name = dot.getField().getName(); | ||
Identifier field = dot.getField(); | ||
CoverageRecorder.getInstance().recordCoverage(field); | ||
String name = field.getName(); | ||
try { | ||
return Starlark.getattr( | ||
fr.thread.mutability(), fr.thread.getSemantics(), object, name, /*defaultValue=*/ null); | ||
|
@@ -627,6 +651,7 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call) | |
Object[] positional = npos == 0 ? EMPTY : new Object[npos]; | ||
for (i = 0; i < npos; i++) { | ||
Argument arg = arguments.get(i); | ||
CoverageRecorder.getInstance().recordCoverage(arg); | ||
Object value = eval(fr, arg.getValue()); | ||
positional[i] = value; | ||
} | ||
|
@@ -635,13 +660,15 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call) | |
Object[] named = n == npos ? EMPTY : new Object[2 * (n - npos)]; | ||
for (int j = 0; i < n; i++) { | ||
Argument.Keyword arg = (Argument.Keyword) arguments.get(i); | ||
CoverageRecorder.getInstance().recordCoverage(arg); | ||
Object value = eval(fr, arg.getValue()); | ||
named[j++] = arg.getName(); | ||
named[j++] = value; | ||
} | ||
|
||
// f(*args) -- varargs | ||
if (star != null) { | ||
CoverageRecorder.getInstance().recordCoverage(star); | ||
Object value = eval(fr, star.getValue()); | ||
if (!(value instanceof StarlarkIterable)) { | ||
fr.setErrorLocation(star.getStartLocation()); | ||
|
@@ -656,6 +683,7 @@ private static Object evalCall(StarlarkThread.Frame fr, CallExpression call) | |
|
||
// f(**kwargs) | ||
if (starstar != null) { | ||
CoverageRecorder.getInstance().recordCoverage(starstar); | ||
Object value = eval(fr, starstar.getValue()); | ||
if (!(value instanceof Dict)) { | ||
fr.setErrorLocation(starstar.getStartLocation()); | ||
|
@@ -791,6 +819,7 @@ void execClauses(int index) throws EvalException, InterruptedException { | |
assign(fr, forClause.getVars(), elem); | ||
execClauses(index + 1); | ||
} | ||
CoverageRecorder.getInstance().recordVirtualJump(clause); | ||
} catch (EvalException ex) { | ||
fr.setErrorLocation(forClause.getStartLocation()); | ||
throw ex; | ||
|
@@ -802,12 +831,15 @@ void execClauses(int index) throws EvalException, InterruptedException { | |
Comprehension.If ifClause = (Comprehension.If) clause; | ||
if (Starlark.truth(eval(fr, ifClause.getCondition()))) { | ||
execClauses(index + 1); | ||
} else { | ||
CoverageRecorder.getInstance().recordVirtualJump(clause); | ||
} | ||
} | ||
return; | ||
} | ||
|
||
// base case: evaluate body and add to result. | ||
CoverageRecorder.getInstance().recordCoverage(comp.getBody()); | ||
if (dict != null) { | ||
DictExpression.Entry body = (DictExpression.Entry) comp.getBody(); | ||
Object k = eval(fr, body.getKey()); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -23,19 +23,23 @@ | |||||||||||||||||||||||||||||||||||||||||||||
import com.google.errorprone.annotations.FormatMethod; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.io.OutputStream; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.io.PrintWriter; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.lang.reflect.Method; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.math.BigInteger; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.time.Duration; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.Set; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.TreeSet; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.function.Function; | ||||||||||||||||||||||||||||||||||||||||||||||
import java.util.regex.Pattern; | ||||||||||||||||||||||||||||||||||||||||||||||
import javax.annotation.Nullable; | ||||||||||||||||||||||||||||||||||||||||||||||
import javax.annotation.concurrent.Immutable; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.annot.StarlarkAnnotations; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.annot.StarlarkBuiltin; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.annot.StarlarkMethod; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.spelling.SpellChecker; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.syntax.CoverageRecorder; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.syntax.Expression; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.syntax.FileOptions; | ||||||||||||||||||||||||||||||||||||||||||||||
import net.starlark.java.syntax.ParserInput; | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -970,4 +974,12 @@ public static boolean startCpuProfile(OutputStream out, Duration period) { | |||||||||||||||||||||||||||||||||||||||||||||
public static void stopCpuProfile() throws IOException { | ||||||||||||||||||||||||||||||||||||||||||||||
CpuProfiler.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
public static void startCoverageCollection(Function<String, Boolean> filenameMatcher) { | ||||||||||||||||||||||||||||||||||||||||||||||
CoverageRecorder.startCoverageCollection(filenameMatcher); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+978
to
+980
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add documentation for startCoverageCollection method This public API method lacks documentation explaining its purpose, when to use it, and how it integrates with the broader coverage system. Consider adding a Javadoc comment to clarify:
+/**
+ * Starts collecting coverage data for Starlark code execution.
+ *
+ * <p>Coverage will be collected for all Starlark files that match the provided filename matcher.
+ * This method should be called before any Starlark code execution for which coverage data is
+ * desired.
+ *
+ * <p>This method is thread-safe and can be called concurrently with Starlark execution.
+ *
+ * @param filenameMatcher a function that returns true for filenames that should have coverage
+ * collected, and false for files to exclude from coverage
+ */
public static void startCoverageCollection(Function<String, Boolean> filenameMatcher) {
CoverageRecorder.startCoverageCollection(filenameMatcher);
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
public static void dumpCoverage(PrintWriter out) { | ||||||||||||||||||||||||||||||||||||||||||||||
CoverageRecorder.getInstance().dump(out); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+982
to
+984
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add documentation and error handling for dumpCoverage method This public API method also lacks documentation and doesn't provide any error handling mechanism. If an error occurs while writing the coverage data, it would silently fail. Consider enhancing this method with proper documentation and error handling: +/**
+ * Writes the collected Starlark coverage data to the specified writer in LCOV format.
+ *
+ * <p>This method should be called after Starlark code execution has completed to generate
+ * the coverage report. It will dump coverage for all files that matched the filter provided
+ * to {@link #startCoverageCollection}.
+ *
+ * <p>The coverage report includes function, line, and branch coverage information.
+ *
+ * @param out the writer to which the LCOV report will be written
+ * @throws RuntimeException if an error occurs while writing the coverage data
+ */
public static void dumpCoverage(PrintWriter out) {
- CoverageRecorder.getInstance().dump(out);
+ try {
+ CoverageRecorder.getInstance().dump(out);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to dump Starlark coverage data", e);
+ }
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leak‑prone I/O: use try‑with‑resources and flush
PrintWriter
FileOutputStream
and the wrappingPrintWriter
are opened but never closed.On long‑running servers this leaks file descriptors, and on short‑lived invocations you may miss buffered bytes because
PrintWriter
is not flushed.This guarantees flushing and closes both streams even if
dumpCoverage
throws.📝 Committable suggestion