Skip to content

Commit c7ea845

Browse files
committed
Implement method coverage
Coverage's methods mode now works and produces the same results as CRuby: { file => { methods: { [owner, name, start_line, start_column, end_line, end_column] => call_count } } } Design follows CRuby's per-method-entry counters and JaCoCo-style probes: * A MethodCoverage counter is created for every method entry (each def or define_method landing in a module, in RubyModule#putMethod) whose source file is being tracked, and attached to the DynamicMethod. Aliases, visibility changes of inherited methods and other forwarding entries get no counter of their own. Their calls count toward the original, as in CRuby. * Every Ruby-level call path (interpreted, mixed-mode, JIT-compiled, and proc-based define_method) hands the counter to the body through the ThreadContext. The body takes it in its first instruction (ReceiveMethodCoverageInstr) and counts the call once its arguments have been received (CoverMethodInstr), so calls that fail on arity, keywords or a raising default value are not counted, matching where CRuby fires the CALL event. Blocks get the same probes since any block may become a method via define_method. Indy call sites no longer bind directly to the compiled handle of a method that is being counted. * Counting is a lock-free atomic add, so threads calling the same method in parallel neither serialize nor lose increments. Entries sharing a key are summed like CRuby. * The parser now records byte columns and end positions of def, block and lambda definitions (DefNode/IterNode source spans), carried into IRScope. Both generated parsers were regenerated from RubyParser.y with jay. Coverage data is now kept per file in FileCoverage (lines, methods, and room for branches), results only contain the requested modes (branches still reports an empty hash), files are listed in parse order, and Coverage.supported?(:methods) is true. Also fixes two pre-existing issues this work surfaced: IRClosure dropped the coverageMode it was given, so blocks converted into methods for define_method were built without any coverage and oneshot_lines probes disarmed themselves when hit while coverage was suspended. Un-excludes the CRuby method coverage tests and the :all-modes spec, and adds JRuby-specific tests for parallel counting and every execution mode. See #5147.
1 parent 336fd77 commit c7ea845

42 files changed

Lines changed: 1138 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎core/src/main/java/org/jruby/RubyModule.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,8 @@ public DynamicMethod putMethod(ThreadContext context, String id, DynamicMethod m
681681
method.setImplementationClass(methodLocation);
682682
}
683683

684+
if (context.runtime.isCoverageEnabled()) context.runtime.getCoverageData().registerMethod(method);
685+
684686
DynamicMethod oldMethod = methodLocation.getMethodsForWrite().put(id, method);
685687

686688
if (oldMethod != null && oldMethod.isRefined()) {

‎core/src/main/java/org/jruby/ast/DefHolder.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class DefHolder {
1414
public IRubyObject value;
1515

1616
public int line;
17+
public int column = -1;
1718
public Object singleton = null;
1819
public ByteList dotOrColon;
1920

‎core/src/main/java/org/jruby/ast/DefNode.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,18 @@ public interface DefNode {
3232
* @return the line (zero-offset)
3333
*/
3434
int getEndLine();
35+
36+
/**
37+
* Column (zero-offset, in bytes) where the source of this definition starts (e.g. at the 'def' keyword or
38+
* the opening brace of a block). -1 if unknown.
39+
* @return the column
40+
*/
41+
int getStartColumn();
42+
43+
/**
44+
* Column (zero-offset, in bytes) just past the end of the source of this definition (e.g. after the 'end'
45+
* keyword or the closing brace of a block). -1 if unknown.
46+
* @return the column
47+
*/
48+
int getEndColumn();
3549
}

‎core/src/main/java/org/jruby/ast/DefnNode.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public DefnNode(int line, RubySymbol name, ArgsNode argsNode, StaticScope scope,
4848
super(line, name, argsNode, scope, bodyNode, endLine);
4949
}
5050

51+
public DefnNode(int line, int startColumn, RubySymbol name, ArgsNode argsNode, StaticScope scope, Node bodyNode,
52+
int endLine, int endColumn) {
53+
super(line, startColumn, name, argsNode, scope, bodyNode, endLine, endColumn);
54+
}
55+
5156
public NodeType getNodeType() {
5257
return NodeType.DEFNNODE;
5358
}

‎core/src/main/java/org/jruby/ast/DefsNode.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ public class DefsNode extends MethodDefNode implements INameNode {
4747

4848
public DefsNode(int line, Node receiverNode, RubySymbol name, ArgsNode argsNode,
4949
StaticScope scope, Node bodyNode, int endLine) {
50-
super(line, name, argsNode, scope, bodyNode, endLine);
50+
this(line, -1, receiverNode, name, argsNode, scope, bodyNode, endLine, -1);
51+
}
52+
53+
public DefsNode(int line, int startColumn, Node receiverNode, RubySymbol name, ArgsNode argsNode,
54+
StaticScope scope, Node bodyNode, int endLine, int endColumn) {
55+
super(line, startColumn, name, argsNode, scope, bodyNode, endLine, endColumn);
5156

5257
assert receiverNode != null : "receiverNode is not null";
5358

‎core/src/main/java/org/jruby/ast/IterNode.java‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public class IterNode extends Node implements DefNode {
4646

4747
// What static scoping relationship exists when it comes into being.
4848
private final StaticScope scope;
49-
private final int endLine;
49+
private int endLine;
50+
private int startColumn = -1;
51+
private int endColumn = -1;
5052

5153
/**
5254
* Used by ForNode only.
@@ -117,4 +119,27 @@ public List<Node> childNodes() {
117119
public int getEndLine() {
118120
return endLine;
119121
}
122+
123+
public int getStartColumn() {
124+
return startColumn;
125+
}
126+
127+
public int getEndColumn() {
128+
return endColumn;
129+
}
130+
131+
/**
132+
* Record the exact source span of this block: from its opening brace/do keyword through its closing
133+
* brace/end keyword (for lambdas: from the start of the parameter list, or just past '->' when there is
134+
* none, through the end of the body).
135+
*
136+
* @param startColumn zero-based byte column of the first character of the block
137+
* @param endLine zero-based line of the last character of the block
138+
* @param endColumn zero-based byte column just past the last character of the block
139+
*/
140+
public void setSourceSpan(int startColumn, int endLine, int endColumn) {
141+
this.startColumn = startColumn;
142+
this.endLine = endLine;
143+
this.endColumn = endColumn;
144+
}
120145
}

‎core/src/main/java/org/jruby/ast/MethodDefNode.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,27 @@ public abstract class MethodDefNode extends Node implements INameNode, DefNode {
4242
protected final StaticScope scope;
4343
protected final Node bodyNode;
4444
protected final int endLine;
45+
protected final int startColumn;
46+
protected final int endColumn;
4547
// We lazily compile methods in IR but Ruby expects next and break to raise syntax error so we will eagerly
4648
// build methods which contain those two keywords.
4749
protected boolean containsNextBreak = false;
4850

4951
public MethodDefNode(int line, RubySymbol name, ArgsNode argsNode, StaticScope scope, Node bodyNode, int endLine) {
52+
this(line, -1, name, argsNode, scope, bodyNode, endLine, -1);
53+
}
54+
55+
public MethodDefNode(int line, int startColumn, RubySymbol name, ArgsNode argsNode, StaticScope scope, Node bodyNode,
56+
int endLine, int endColumn) {
5057
super(line, bodyNode.containsVariableAssignment());
5158

5259
this.name = name;
5360
this.argsNode = argsNode;
5461
this.scope = scope;
5562
this.bodyNode = bodyNode;
5663
this.endLine = endLine;
64+
this.startColumn = startColumn;
65+
this.endColumn = endColumn;
5766
}
5867

5968
/**
@@ -98,6 +107,14 @@ public int getEndLine() {
98107
return endLine;
99108
}
100109

110+
public int getStartColumn() {
111+
return startColumn;
112+
}
113+
114+
public int getEndColumn() {
115+
return endColumn;
116+
}
117+
101118
public void setContainsNextBreak() {
102119
containsNextBreak = true;
103120
}

0 commit comments

Comments
 (0)