summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--compile.c4
-rw-r--r--ext/coverage/coverage.c61
-rw-r--r--ext/coverage/lib/coverage.rb14
-rw-r--r--internal.h2
-rw-r--r--iseq.c24
-rw-r--r--test/coverage/test_coverage.rb192
-rw-r--r--thread.c50
-rw-r--r--vm_core.h1
8 files changed, 312 insertions, 36 deletions
diff --git a/compile.c b/compile.c
index 272b9dd..6e6825d 100644
--- a/compile.c
+++ b/compile.c
@@ -2013,7 +2013,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor)
sp = calc_sp_depth(sp, iobj);
code_index += insn_data_length(iobj);
insn_num++;
- if (ISEQ_COVERAGE(iseq) && ISEQ_LINE_COVERAGE(iseq) && (events & RUBY_EVENT_COVERAGE_LINE)) {
+ if (ISEQ_COVERAGE(iseq) && ISEQ_LINE_COVERAGE(iseq) &&
+ (events & RUBY_EVENT_COVERAGE_LINE) &&
+ !(rb_get_coverage_mode() & COVERAGE_TARGET_ONESHOT_LINES)) {
int line = iobj->insn_info.line_no;
RARRAY_ASET(ISEQ_LINE_COVERAGE(iseq), line - 1, INT2FIX(0));
}
diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c
index f46955a..cf6e0a8 100644
--- a/ext/coverage/coverage.c
+++ b/ext/coverage/coverage.c
@@ -45,9 +45,12 @@ rb_coverage_start(int argc, VALUE *argv, VALUE klass)
mode |= COVERAGE_TARGET_BRANCHES;
if (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("methods")))))
mode |= COVERAGE_TARGET_METHODS;
- if (mode == 0) {
- rb_raise(rb_eRuntimeError, "no measuring target is specified");
- }
+ if (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("oneshot_lines"))))) {
+ if (mode & COVERAGE_TARGET_LINES)
+ rb_raise(rb_eRuntimeError, "cannot enable lines and oneshot_lines simultaneously");
+ mode |= COVERAGE_TARGET_LINES;
+ mode |= COVERAGE_TARGET_ONESHOT_LINES;
+ }
}
if (mode & COVERAGE_TARGET_METHODS) {
@@ -179,9 +182,10 @@ coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h)
if (current_mode & COVERAGE_TARGET_LINES) {
VALUE lines = RARRAY_AREF(coverage, COVERAGE_INDEX_LINES);
+ const char *kw = (current_mode & COVERAGE_TARGET_ONESHOT_LINES) ? "oneshot_lines" : "lines";
lines = rb_ary_dup(lines);
rb_ary_freeze(lines);
- rb_hash_aset(h, ID2SYM(rb_intern("lines")), lines);
+ rb_hash_aset(h, ID2SYM(rb_intern(kw)), lines);
}
if (current_mode & COVERAGE_TARGET_BRANCHES) {
@@ -205,6 +209,7 @@ coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h)
* Coverage.peek_result => hash
*
* Returns a hash that contains filename as key and coverage array as value.
+ * This is the same as `Coverage.result(stop: false, clear: false)`.
*
* {
* "file.rb" => [1, 2, nil],
@@ -229,22 +234,54 @@ rb_coverage_peek_result(VALUE klass)
return ncoverages;
}
+
+static int
+clear_me2counter_i(VALUE key, VALUE value, VALUE unused)
+{
+ rb_hash_aset(me2counter, key, INT2FIX(0));
+ return ST_CONTINUE;
+}
+
/*
* call-seq:
- * Coverage.result => hash
+ * Coverage.result(stop: true, clear: true) => hash
*
- * Returns a hash that contains filename as key and coverage array as value
- * and disables coverage measurement.
+ * Returns a hash that contains filename as key and coverage array as value.
+ * If +clear+ is true, it clears the counters to zero.
+ * If +stop+ is true, it disables coverage measurement.
*/
static VALUE
-rb_coverage_result(VALUE klass)
+rb_coverage_result(int argc, VALUE *argv, VALUE klass)
{
- VALUE ncoverages = rb_coverage_peek_result(klass);
- rb_reset_coverages();
- me2counter = Qnil;
+ VALUE ncoverages;
+ VALUE opt;
+ int stop = 1, clear = 1;
+
+ rb_scan_args(argc, argv, "01", &opt);
+
+ if (argc == 1) {
+ opt = rb_convert_type(opt, T_HASH, "Hash", "to_hash");
+ stop = RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("stop"))));
+ clear = RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("clear"))));
+ }
+
+ ncoverages = rb_coverage_peek_result(klass);
+ if (stop && !clear) {
+ rb_warn("stop implies clear");
+ clear = 1;
+ }
+ if (clear) {
+ rb_clear_coverages();
+ if (!NIL_P(me2counter)) rb_hash_foreach(me2counter, clear_me2counter_i, Qnil);
+ }
+ if (stop) {
+ rb_reset_coverages();
+ me2counter = Qnil;
+ }
return ncoverages;
}
+
/*
* call-seq:
* Coverage.running? => bool
@@ -297,7 +334,7 @@ Init_coverage(void)
{
VALUE rb_mCoverage = rb_define_module("Coverage");
rb_define_module_function(rb_mCoverage, "start", rb_coverage_start, -1);
- rb_define_module_function(rb_mCoverage, "result", rb_coverage_result, 0);
+ rb_define_module_function(rb_mCoverage, "result", rb_coverage_result, -1);
rb_define_module_function(rb_mCoverage, "peek_result", rb_coverage_peek_result, 0);
rb_define_module_function(rb_mCoverage, "running?", rb_coverage_running, 0);
rb_global_variable(&me2counter);
diff --git a/ext/coverage/lib/coverage.rb b/ext/coverage/lib/coverage.rb
new file mode 100644
index 0000000..7a6ef0e
--- /dev/null
+++ b/ext/coverage/lib/coverage.rb
@@ -0,0 +1,14 @@
+require "coverage.so"
+
+module Coverage
+ def self.line_stub(file)
+ lines = File.foreach(file).map { nil }
+ iseqs = [RubyVM::InstructionSequence.compile_file(file)]
+ until iseqs.empty?
+ iseq = iseqs.pop
+ iseq.trace_points.each {|n, _| lines[n - 1] = 0 }
+ iseq.each_child {|child| iseqs << child }
+ end
+ lines
+ end
+end
diff --git a/internal.h b/internal.h
index f6fcd39..ce7d98a 100644
--- a/internal.h
+++ b/internal.h
@@ -1852,12 +1852,14 @@ struct timeval rb_time_timeval(VALUE);
#define COVERAGE_TARGET_LINES 1
#define COVERAGE_TARGET_BRANCHES 2
#define COVERAGE_TARGET_METHODS 4
+#define COVERAGE_TARGET_ONESHOT_LINES 8
VALUE rb_obj_is_mutex(VALUE obj);
VALUE rb_suppress_tracing(VALUE (*func)(VALUE), VALUE arg);
void rb_thread_execute_interrupts(VALUE th);
void rb_clear_trace_func(void);
VALUE rb_get_coverages(void);
+int rb_get_coverage_mode(void);
VALUE rb_default_coverage(int);
VALUE rb_thread_shield_new(void);
VALUE rb_thread_shield_wait(VALUE self);
diff --git a/iseq.c b/iseq.c
index 343fc9e..243afc2 100644
--- a/iseq.c
+++ b/iseq.c
@@ -656,7 +656,8 @@ rb_iseq_new_top(const rb_ast_body_t *ast, VALUE name, VALUE path, VALUE realpath
VALUE coverages = rb_get_coverages();
if (RTEST(coverages)) {
if (ast->line_count >= 0) {
- VALUE coverage = rb_default_coverage(ast->line_count);
+ int len = (rb_get_coverage_mode() & COVERAGE_TARGET_ONESHOT_LINES) ? 0 : ast->line_count;
+ VALUE coverage = rb_default_coverage(len);
rb_hash_aset(coverages, path, coverage);
}
}
@@ -1655,6 +1656,19 @@ rb_iseq_event_flags(const rb_iseq_t *iseq, size_t pos)
}
}
+void
+rb_iseq_clear_event_flags(const rb_iseq_t *iseq, size_t pos, rb_event_flag_t reset)
+{
+ struct iseq_insn_info_entry *entry = (struct iseq_insn_info_entry *)get_insn_info(iseq, pos);
+ if (entry) {
+ entry->events &= ~reset;
+ if (!(entry->events & iseq->aux.trace_events)) {
+ void rb_iseq_trace_flag_cleared(const rb_iseq_t *iseq, int pos);
+ rb_iseq_trace_flag_cleared(iseq, pos);
+ }
+ }
+}
+
static VALUE
local_var_name(const rb_iseq_t *diseq, VALUE level, VALUE op)
{
@@ -2935,6 +2949,14 @@ encoded_iseq_trace_instrument(VALUE *iseq_encoded_insn, rb_event_flag_t turnon)
}
void
+rb_iseq_trace_flag_cleared(const rb_iseq_t *iseq, int pos)
+{
+ const struct rb_iseq_constant_body *const body = iseq->body;
+ VALUE *iseq_encoded = (VALUE *)body->iseq_encoded;
+ encoded_iseq_trace_instrument(&iseq_encoded[pos], 0);
+}
+
+void
rb_iseq_trace_set(const rb_iseq_t *iseq, rb_event_flag_t turnon_events)
{
VM_ASSERT((turnon_events & ~ISEQ_TRACE_EVENTS) == 0);
diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb
index 8cc6253..1240833 100644
--- a/test/coverage/test_coverage.rb
+++ b/test/coverage/test_coverage.rb
@@ -483,4 +483,196 @@ class TestCoverage < Test::Unit::TestCase
}
assert_coverage(code, { methods: true }, result)
end
+
+ def test_oneshot_line_coverage
+ result = {
+ :oneshot_lines => [2, 6, 10, 12, 17, 18, 25, 20]
+ }
+ assert_coverage(<<~"end;", { oneshot_lines: true }, result)
+ FOO = [
+ { foo: 'bar' }, # 2
+ { bar: 'baz' }
+ ]
+
+ 'some string'.split # 6
+ .map(&:length)
+
+ some =
+ 'value' # 10
+
+ Struct.new( # 12
+ :foo,
+ :bar
+ ).new
+
+ class Test # 17
+ def foo(bar) # 18
+ {
+ foo: bar # 20
+ }
+ end
+ end
+
+ Test.new.foo(Object.new) # 25
+ end;
+ end
+
+ def test_clear_with_lines
+ Dir.mktmpdir {|tmp|
+ Dir.chdir(tmp) {
+ File.open("test.rb", "w") do |f|
+ f.puts "def foo(x)"
+ f.puts " if x > 0"
+ f.puts " :pos"
+ f.puts " else"
+ f.puts " :non_pos"
+ f.puts " end"
+ f.puts "end"
+ end
+
+ exp = [
+ "{:lines=>[1, 0, 0, nil, 0, nil, nil]}",
+ "{:lines=>[0, 1, 1, nil, 0, nil, nil]}",
+ "{:lines=>[0, 1, 0, nil, 1, nil, nil]}",
+ ]
+ assert_in_out_err(%w[-rcoverage], <<-"end;", exp, [])
+ Coverage.start(lines: true)
+ tmp = Dir.pwd
+ f = tmp + "/test.rb"
+ require f
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result[f]
+ end;
+ }
+ }
+ end
+
+ def test_clear_with_branches
+ Dir.mktmpdir {|tmp|
+ Dir.chdir(tmp) {
+ File.open("test.rb", "w") do |f|
+ f.puts "def foo(x)"
+ f.puts " if x > 0"
+ f.puts " :pos"
+ f.puts " else"
+ f.puts " :non_pos"
+ f.puts " end"
+ f.puts "end"
+ end
+
+ exp = [
+ "{:branches=>{[:if, 0, 2, 2, 6, 5]=>{[:then, 1, 3, 4, 3, 8]=>0, [:else, 2, 5, 4, 5, 12]=>0}}}",
+ "{:branches=>{[:if, 0, 2, 2, 6, 5]=>{[:then, 1, 3, 4, 3, 8]=>1, [:else, 2, 5, 4, 5, 12]=>0}}}",
+ "{:branches=>{[:if, 0, 2, 2, 6, 5]=>{[:then, 1, 3, 4, 3, 8]=>0, [:else, 2, 5, 4, 5, 12]=>1}}}",
+ "{:branches=>{[:if, 0, 2, 2, 6, 5]=>{[:then, 1, 3, 4, 3, 8]=>0, [:else, 2, 5, 4, 5, 12]=>1}}}",
+ ]
+ assert_in_out_err(%w[-rcoverage], <<-"end;", exp, [])
+ Coverage.start(branches: true)
+ tmp = Dir.pwd
+ f = tmp + "/test.rb"
+ require f
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ end;
+ }
+ }
+ end
+
+ def test_clear_with_methods
+ Dir.mktmpdir {|tmp|
+ Dir.chdir(tmp) {
+ File.open("test.rb", "w") do |f|
+ f.puts "def foo(x)"
+ f.puts " if x > 0"
+ f.puts " :pos"
+ f.puts " else"
+ f.puts " :non_pos"
+ f.puts " end"
+ f.puts "end"
+ end
+
+ exp = [
+ "{:methods=>{[Object, :foo, 1, 0, 7, 3]=>0}}",
+ "{:methods=>{[Object, :foo, 1, 0, 7, 3]=>1}}",
+ "{:methods=>{[Object, :foo, 1, 0, 7, 3]=>1}}",
+ "{:methods=>{[Object, :foo, 1, 0, 7, 3]=>1}}"
+ ]
+ assert_in_out_err(%w[-rcoverage], <<-"end;", exp, [])
+ Coverage.start(methods: true)
+ tmp = Dir.pwd
+ f = tmp + "/test.rb"
+ require f
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ end;
+ }
+ }
+ end
+
+ def test_clear_with_oneshot_lines
+ Dir.mktmpdir {|tmp|
+ Dir.chdir(tmp) {
+ File.open("test.rb", "w") do |f|
+ f.puts "def foo(x)"
+ f.puts " if x > 0"
+ f.puts " :pos"
+ f.puts " else"
+ f.puts " :non_pos"
+ f.puts " end"
+ f.puts "end"
+ end
+
+ exp = [
+ "{:oneshot_lines=>[1]}",
+ "{:oneshot_lines=>[2, 3]}",
+ "{:oneshot_lines=>[5]}",
+ "{:oneshot_lines=>[]}",
+ ]
+ assert_in_out_err(%w[-rcoverage], <<-"end;", exp, [])
+ Coverage.start(oneshot_lines: true)
+ tmp = Dir.pwd
+ f = tmp + "/test.rb"
+ require f
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ foo(-1)
+ p Coverage.result(stop: false, clear: true)[f]
+ end;
+ }
+ }
+ end
+
+ def test_line_stub
+ Dir.mktmpdir {|tmp|
+ Dir.chdir(tmp) {
+ File.open("test.rb", "w") do |f|
+ f.puts "def foo(x)"
+ f.puts " if x > 0"
+ f.puts " :pos"
+ f.puts " else"
+ f.puts " :non_pos"
+ f.puts " end"
+ f.puts "end"
+ end
+
+ assert_equal([0, 0, 0, nil, 0, nil, 0], Coverage.line_stub("test.rb"))
+ }
+ }
+ end
end
diff --git a/thread.c b/thread.c
index b4baff6..443efb1 100644
--- a/thread.c
+++ b/thread.c
@@ -4323,11 +4323,16 @@ clear_coverage_i(st_data_t key, st_data_t val, st_data_t dummy)
VALUE branches = RARRAY_AREF(coverage, COVERAGE_INDEX_BRANCHES);
if (lines) {
- for (i = 0; i < RARRAY_LEN(lines); i++) {
- if (RARRAY_AREF(lines, i) != Qnil) {
- RARRAY_ASET(lines, i, INT2FIX(0));
- }
- }
+ if (GET_VM()->coverage_mode & COVERAGE_TARGET_ONESHOT_LINES) {
+ rb_ary_clear(lines);
+ }
+ else {
+ int i;
+ for (i = 0; i < RARRAY_LEN(lines); i++) {
+ if (RARRAY_AREF(lines, i) != Qnil)
+ RARRAY_ASET(lines, i, INT2FIX(0));
+ }
+ }
}
if (branches) {
VALUE counters = RARRAY_AREF(branches, 1);
@@ -4339,8 +4344,8 @@ clear_coverage_i(st_data_t key, st_data_t val, st_data_t dummy)
return ST_CONTINUE;
}
-static void
-clear_coverage(void)
+void
+rb_clear_coverages(void)
{
VALUE coverages = rb_get_coverages();
if (RTEST(coverages)) {
@@ -4373,7 +4378,7 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r
vm->fork_gen++;
vm->sleeper = 0;
- clear_coverage();
+ rb_clear_coverages();
}
static void
@@ -5219,13 +5224,20 @@ rb_check_deadlock(rb_vm_t *vm)
static void
update_line_coverage(VALUE data, const rb_trace_arg_t *trace_arg)
{
- VALUE coverage = rb_iseq_coverage(GET_EC()->cfp->iseq);
+ const rb_control_frame_t *cfp = GET_EC()->cfp;
+ VALUE coverage = rb_iseq_coverage(cfp->iseq);
if (RB_TYPE_P(coverage, T_ARRAY) && !RBASIC_CLASS(coverage)) {
VALUE lines = RARRAY_AREF(coverage, COVERAGE_INDEX_LINES);
if (lines) {
long line = rb_sourceline() - 1;
long count;
VALUE num;
+ void rb_iseq_clear_event_flags(const rb_iseq_t *iseq, size_t pos, rb_event_flag_t reset);
+ if (GET_VM()->coverage_mode & COVERAGE_TARGET_ONESHOT_LINES) {
+ rb_iseq_clear_event_flags(cfp->iseq, cfp->pc - cfp->iseq->body->iseq_encoded - 1, RUBY_EVENT_COVERAGE_LINE);
+ rb_ary_push(lines, LONG2FIX(line + 1));
+ return;
+ }
if (line >= RARRAY_LEN(lines)) { /* no longer tracked */
return;
}
@@ -5340,6 +5352,12 @@ rb_get_coverages(void)
return GET_VM()->coverages;
}
+int
+rb_get_coverage_mode(void)
+{
+ return GET_VM()->coverage_mode;
+}
+
void
rb_set_coverages(VALUE coverages, int mode, VALUE me2counter)
{
@@ -5355,22 +5373,10 @@ rb_set_coverages(VALUE coverages, int mode, VALUE me2counter)
}
/* Make coverage arrays empty so old covered files are no longer tracked. */
-static int
-reset_coverage_i(st_data_t key, st_data_t val, st_data_t dummy)
-{
- VALUE coverage = (VALUE)val;
- VALUE lines = RARRAY_AREF(coverage, COVERAGE_INDEX_LINES);
- VALUE branches = RARRAY_AREF(coverage, COVERAGE_INDEX_BRANCHES);
- if (lines) rb_ary_clear(lines);
- if (branches) rb_ary_clear(branches);
- return ST_CONTINUE;
-}
-
void
rb_reset_coverages(void)
{
- VALUE coverages = rb_get_coverages();
- st_foreach(rb_hash_tbl_raw(coverages), reset_coverage_i, 0);
+ rb_clear_coverages();
rb_iseq_remove_coverage_all();
GET_VM()->coverages = Qfalse;
rb_remove_event_hook((rb_event_hook_func_t) update_line_coverage);
diff --git a/vm_core.h b/vm_core.h
index 7b795bb..4443031 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -1837,6 +1837,7 @@ int rb_thread_check_trap_pending(void);
extern VALUE rb_get_coverages(void);
extern void rb_set_coverages(VALUE, int, VALUE);
+extern void rb_clear_coverages(void);
extern void rb_reset_coverages(void);
void rb_postponed_job_flush(rb_vm_t *vm);