Skip to content

Commit 8ddaa56

Browse files
authored
Merge pull request #6180 from headius/new_coverage
Implement oneshot_lines for coverage
2 parents 8a88c1d + 1452994 commit 8ddaa56

File tree

19 files changed

+339
-151
lines changed

19 files changed

+339
-151
lines changed

core/src/main/java/org/jruby/ast/RootNode.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import org.jruby.ParseResult;
3535
import org.jruby.ast.visitor.NodeVisitor;
36+
import org.jruby.ext.coverage.CoverageData;
3637
import org.jruby.lexer.yacc.ISourcePosition;
3738
import org.jruby.parser.StaticScope;
3839
import org.jruby.runtime.DynamicScope;
@@ -51,28 +52,28 @@ public class RootNode extends Node implements ParseResult {
5152
private Node bodyNode;
5253
private String file;
5354
private int endPosition;
54-
private boolean needsCodeCoverage;
55+
private final int coverageMode;
5556

5657
public RootNode(ISourcePosition position, DynamicScope scope, Node bodyNode, String file) {
57-
this(position, scope, bodyNode, file, -1, false);
58+
this(position, scope, bodyNode, file, -1, CoverageData.NONE);
5859
}
5960

60-
public RootNode(ISourcePosition position, DynamicScope scope, Node bodyNode, String file, int endPosition, boolean needsCodeCoverage) {
61+
public RootNode(ISourcePosition position, DynamicScope scope, Node bodyNode, String file, int endPosition, int coverageMode) {
6162
super(position, bodyNode.containsVariableAssignment());
6263

6364
this.scope = scope;
6465
this.staticScope = scope.getStaticScope();
6566
this.bodyNode = bodyNode;
6667
this.file = file;
6768
this.endPosition = endPosition;
68-
this.needsCodeCoverage = needsCodeCoverage;
69+
this.coverageMode = coverageMode;
6970

7071
staticScope.setFile(file);
7172
}
7273

7374
@Deprecated
7475
public RootNode(ISourcePosition position, DynamicScope scope, Node bodyNode, String file, int endPosition) {
75-
this(position, scope, bodyNode, file, endPosition, false);
76+
this(position, scope, bodyNode, file, endPosition, CoverageData.NONE);
7677
}
7778

7879
public NodeType getNodeType() {
@@ -133,8 +134,8 @@ public int getEndPosition() {
133134
}
134135

135136
// Is coverage enabled and is this a valid source file for coverage to apply?
136-
public boolean needsCoverage() {
137-
return needsCodeCoverage;
137+
public int coverageMode() {
138+
return coverageMode;
138139
}
139140

140141
@Override

core/src/main/java/org/jruby/ext/coverage/CoverageData.java

Lines changed: 59 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,78 @@
2626

2727
package org.jruby.ext.coverage;
2828

29-
import java.util.Arrays;
30-
import java.util.EnumSet;
3129
import java.util.HashMap;
3230
import java.util.Map;
33-
import org.jruby.Ruby;
34-
import org.jruby.runtime.EventHook;
35-
import org.jruby.runtime.RubyEvent;
36-
import org.jruby.runtime.ThreadContext;
37-
import org.jruby.runtime.builtin.IRubyObject;
31+
import org.jruby.util.collections.IntList;
3832

3933
public class CoverageData {
4034
public static final String STARTED = ""; // no load/require ruby file can be "" so we
41-
private static final int[] SVALUE = new int[0]; // use it as a holder to know if start occurs
42-
private volatile Map<String, int[]> coverage;
35+
private static final IntList SVALUE = new IntList(); // use it as a holder to know if start occurs
36+
private volatile Map<String, IntList> coverage;
37+
private volatile int mode;
38+
39+
public static final int NONE = 0;
40+
public static final int LINES = 1 << 0;
41+
public static final int BRANCHES = 1 << 1;
42+
public static final int METHODS = 1 << 2;
43+
public static final int ONESHOT_LINES = 1 << 3;
44+
public static final int ALL = LINES | BRANCHES | METHODS;
4345

4446
public boolean isCoverageEnabled() {
45-
return coverage != null && coverage.get(STARTED) != null;
47+
return mode != 0;
48+
}
49+
50+
public int getMode() {
51+
return mode;
4652
}
4753

48-
public Map<String, int[]> getCoverage() {
54+
public boolean isOneshot() {
55+
return (mode & ONESHOT_LINES) != 0;
56+
}
57+
58+
public Map<String, IntList> getCoverage() {
4959
return coverage;
5060
}
5161

52-
public synchronized void setCoverageEnabled(Ruby runtime, boolean enabled) {
53-
Map<String, int[]> coverage = this.coverage;
62+
/**
63+
* Update coverage data for the given file and line number.
64+
*
65+
* @param filename
66+
* @param line
67+
*/
68+
public synchronized void coverLine(String filename, int line) {
69+
IntList lines = coverage.get(filename);
70+
71+
if (lines == null) return;
72+
73+
if (isOneshot()) {
74+
lines.add(line);
75+
} else {
76+
if (lines.size() <= line) return;
77+
lines.set(line, lines.get(line) + 1);
78+
}
79+
}
80+
81+
public synchronized void setCoverageEnabled(int mode) {
82+
Map<String, IntList> coverage = this.coverage;
5483

55-
if (coverage == null) coverage = new HashMap<String, int[]>();
84+
if (coverage == null) coverage = new HashMap<>();
5685

57-
if (enabled) {
86+
if (mode != CoverageData.NONE) {
5887
coverage.put(STARTED, SVALUE);
59-
runtime.addEventHook(COVERAGE_HOOK);
6088
} else {
6189
coverage.remove(STARTED);
6290
}
6391

6492
this.coverage = coverage;
93+
this.mode = mode;
6594
}
6695

67-
public synchronized Map<String, int[]> resetCoverage(Ruby runtime) {
68-
Map<String, int[]> coverage = this.coverage;
69-
runtime.removeEventHook(COVERAGE_HOOK);
96+
public synchronized Map<String, IntList> resetCoverage() {
97+
Map<String, IntList> coverage = this.coverage;
7098
coverage.remove(STARTED);
7199

72-
73-
for (Map.Entry<String, int[]> entry : coverage.entrySet()) {
100+
for (Map.Entry<String, IntList> entry : coverage.entrySet()) {
74101
String key = entry.getKey();
75102

76103
// on reset we do not reset files where no execution ever happened but we do reset
@@ -79,22 +106,21 @@ public synchronized Map<String, int[]> resetCoverage(Ruby runtime) {
79106
}
80107

81108
this.coverage = null;
109+
this.mode = CoverageData.NONE;
82110

83111
return coverage;
84112
}
85113

86-
private static boolean hasCodeBeenPartiallyCovered(int[] lines) {
87-
for (int i = 0; i < lines.length; i++) {
88-
if (lines[i] > 0) return true;
114+
private static boolean hasCodeBeenPartiallyCovered(IntList lines) {
115+
for (int i = 0; i < lines.size(); i++) {
116+
if (lines.get(i) > 0) return true;
89117
}
90118

91119
return false;
92120
}
93121

94-
public synchronized Map<String, int[]> prepareCoverage(String filename, int[] lines) {
95-
assert lines != null;
96-
97-
Map<String, int[]> coverage = this.coverage;
122+
public synchronized Map<String, IntList> prepareCoverage(String filename, int[] startingLines) {
123+
Map<String, IntList> coverage = this.coverage;
98124

99125
if (filename == null) {
100126
// null filename from certain evals, Ruby.executeScript, etc (jruby/jruby#5111)
@@ -103,45 +129,14 @@ public synchronized Map<String, int[]> prepareCoverage(String filename, int[] li
103129
}
104130

105131
if (coverage != null) {
106-
coverage.put(filename, lines);
132+
if (isOneshot()) {
133+
coverage.put(filename, new IntList());
134+
} else {
135+
coverage.put(filename, new IntList(startingLines));
136+
}
107137
}
108138

109139
return coverage;
110140
}
111-
112-
private static final EnumSet<RubyEvent> COVERAGE_EVENTS = EnumSet.of(RubyEvent.COVERAGE);
113-
114-
private final EventHook COVERAGE_HOOK = new EventHook() {
115-
@Override
116-
public void eventHandler(ThreadContext context, String eventName, String file, int line, String name, IRubyObject type) {
117-
synchronized (CoverageData.this) {
118-
Map<String, int[]> coverage = CoverageData.this.coverage;
119-
120-
// Should not be needed but I predict serialization of IR might hit this.
121-
if (coverage == null || line <= 0) return;
122-
123-
int[] lines = coverage.get(file);
124-
125-
// no coverage lines for this record. bail out (should never happen)
126-
if (lines == null) return;
127-
128-
// coverage is dead for this record. result() has been called once and we marked it as such as an empty list.
129-
if (lines.length == 0) return;
130-
131-
// increment usage count by one.
132-
lines[line - 1] += 1;
133-
}
134-
}
135-
136-
@Override
137-
public boolean isInterestedInEvent(RubyEvent event) {
138-
return event == RubyEvent.COVERAGE;
139-
}
140-
141-
@Override
142-
public EnumSet<RubyEvent> eventSet() {
143-
return COVERAGE_EVENTS;
144-
}
145-
};
146141

147142
}

core/src/main/java/org/jruby/ext/coverage/CoverageModule.java

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
import org.jruby.RubyHash;
3333
import org.jruby.RubyString;
3434
import org.jruby.anno.JRubyMethod;
35+
import org.jruby.ast.util.ArgsUtil;
3536
import org.jruby.runtime.ThreadContext;
3637
import org.jruby.runtime.builtin.IRubyObject;
38+
import org.jruby.util.collections.IntList;
3739

3840
/**
3941
* Implementation of Ruby 1.9.2's "Coverage" module
@@ -44,54 +46,109 @@ public static IRubyObject start(ThreadContext context, IRubyObject self) {
4446
Ruby runtime = context.runtime;
4547

4648
if (!runtime.getCoverageData().isCoverageEnabled()) {
47-
runtime.getCoverageData().setCoverageEnabled(runtime, true);
49+
runtime.getCoverageData().setCoverageEnabled(CoverageData.LINES);
4850
}
4951

5052
return context.nil;
5153
}
5254

5355
@JRubyMethod(module = true)
54-
public static IRubyObject result(ThreadContext context, IRubyObject self) {
56+
public static IRubyObject start(ThreadContext context, IRubyObject self, IRubyObject opts) {
5557
Ruby runtime = context.runtime;
56-
58+
5759
if (!runtime.getCoverageData().isCoverageEnabled()) {
60+
int mode = 0;
61+
62+
if (ArgsUtil.extractKeywordArg(context, "all", opts).isTrue()) {
63+
mode |= CoverageData.ALL;
64+
} else {
65+
if (ArgsUtil.extractKeywordArg(context, "lines", opts).isTrue()) {
66+
mode |= CoverageData.LINES;
67+
}
68+
if (ArgsUtil.extractKeywordArg(context, "branches", opts).isTrue()) {
69+
runtime.getWarnings().warn("branch coverage is not supported");
70+
mode |= CoverageData.BRANCHES;
71+
}
72+
if (ArgsUtil.extractKeywordArg(context, "methods", opts).isTrue()) {
73+
runtime.getWarnings().warn("method coverage is not supported");
74+
mode |= CoverageData.METHODS;
75+
}
76+
if (ArgsUtil.extractKeywordArg(context, "oneshot_lines", opts).isTrue()) {
77+
mode |= CoverageData.LINES;
78+
mode |= CoverageData.ONESHOT_LINES;
79+
}
80+
}
81+
82+
runtime.getCoverageData().setCoverageEnabled(mode);
83+
}
84+
85+
return context.nil;
86+
}
87+
88+
@JRubyMethod(module = true)
89+
public static IRubyObject result(ThreadContext context, IRubyObject self) {
90+
Ruby runtime = context.runtime;
91+
92+
CoverageData coverageData = runtime.getCoverageData();
93+
94+
if (!coverageData.isCoverageEnabled()) {
5895
throw runtime.newRuntimeError("coverage measurement is not enabled");
5996
}
6097

61-
IRubyObject result = convertCoverageToRuby(context, runtime, runtime.getCoverageData().getCoverage());
62-
runtime.getCoverageData().resetCoverage(runtime);
98+
IRubyObject result = convertCoverageToRuby(context, runtime, coverageData.getCoverage(), coverageData.getMode());
99+
100+
coverageData.resetCoverage();
101+
63102
return result;
64103
}
65104

66105
@JRubyMethod(module = true)
67106
public static IRubyObject peek_result(ThreadContext context, IRubyObject self) {
68107
Ruby runtime = context.runtime;
69108

70-
if (!runtime.getCoverageData().isCoverageEnabled()) {
109+
CoverageData coverageData = runtime.getCoverageData();
110+
111+
if (!coverageData.isCoverageEnabled()) {
71112
throw runtime.newRuntimeError("coverage measurement is not enabled");
72113
}
73114

74-
return convertCoverageToRuby(context, runtime, runtime.getCoverageData().getCoverage());
115+
return convertCoverageToRuby(context, runtime, coverageData.getCoverage(), coverageData.getMode());
75116
}
76117

77118
@JRubyMethod(name = "running?", module = true)
78119
public static IRubyObject running_p(ThreadContext context, IRubyObject self) {
79120
return context.runtime.getCoverageData().isCoverageEnabled() ? context.tru : context.fals;
80121
}
81122

82-
private static IRubyObject convertCoverageToRuby(ThreadContext context, Ruby runtime, Map<String, int[]> coverage) {
123+
private static IRubyObject convertCoverageToRuby(ThreadContext context, Ruby runtime, Map<String, IntList> coverage, int mode) {
83124
// populate a Ruby Hash with coverage data
84125
RubyHash covHash = RubyHash.newHash(runtime);
85-
for (Map.Entry<String, int[]> entry : coverage.entrySet()) {
126+
for (Map.Entry<String, IntList> entry : coverage.entrySet()) {
86127
if (entry.getKey().equals(CoverageData.STARTED)) continue; // ignore our hidden marker
87128

88-
final int[] val = entry.getValue();
89-
RubyArray ary = RubyArray.newArray(runtime, val.length);
90-
for (int i = 0; i < val.length; i++) {
91-
int integer = val[i];
92-
ary.store(i, integer == -1 ? context.nil : runtime.newFixnum(integer));
129+
final IntList val = entry.getValue();
130+
boolean oneshot = (mode & CoverageData.ONESHOT_LINES) != 0;
131+
132+
RubyArray ary = RubyArray.newArray(runtime, val.size());
133+
for (int i = 0; i < val.size(); i++) {
134+
int integer = val.get(i);
135+
if (oneshot) {
136+
ary.push(runtime.newFixnum(integer + 1));
137+
} else {
138+
ary.store(i, integer == -1 ? context.nil : runtime.newFixnum(integer));
139+
}
140+
}
141+
142+
RubyString key = RubyString.newString(runtime, entry.getKey());
143+
IRubyObject value = ary;
144+
145+
if (oneshot) {
146+
RubyHash oneshotHash = RubyHash.newSmallHash(runtime);
147+
oneshotHash.fastASetSmall(runtime.newSymbol("oneshot_lines"), ary);
148+
value = oneshotHash;
93149
}
94-
covHash.fastASetCheckString(runtime, RubyString.newString(runtime, entry.getKey()), ary);
150+
151+
covHash.fastASetCheckString(runtime, key, value);
95152
}
96153

97154
return covHash;

0 commit comments

Comments
 (0)