From 510205749d6dd292043a5d0ddb7968d3ff5b8135 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Mon, 20 Jan 2014 19:42:44 -0800 Subject: [PATCH 1/8] Adding method-level coverage --- compile.c | 12 ++++++++++++ ext/coverage/coverage.c | 8 ++++++-- include/ruby/ruby.h | 2 ++ iseq.c | 8 +++++++- parse.y | 6 +++++- thread.c | 22 +++++++++++++++++++++- vm_core.h | 3 ++- 7 files changed, 55 insertions(+), 6 deletions(-) diff --git a/compile.c b/compile.c index 849d4a5..f563a0a 100644 --- a/compile.c +++ b/compile.c @@ -226,6 +226,17 @@ struct iseq_compile_data_ensure_node_stack { iseq->compile_data->last_coverable_line = (line); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_COVERAGE)); \ } \ + if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ + /*iseq->compile_data->last_coverable_line = (line); \ + VALUE info = rb_hash_new(); \ + rb_hash_aset(info, ID2SYM(rb_intern("count")), INT2FIX(0));*/ \ + rb_hash_aset(iseq->method_coverage, LONG2FIX(line), Qnil); \ + } \ + if ((event) == RUBY_EVENT_CALL && iseq->method_coverage && \ + (line) != iseq->compile_data->last_coverable_line) { \ + iseq->compile_data->last_coverable_line = (line); \ + ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_MCOVERAGE)); \ + } \ if (iseq->compile_data->option->trace_instruction) { \ ADD_INSN1((seq), (line), trace, INT2FIX(event)); \ } \ @@ -4938,6 +4949,7 @@ enum compile_array_type_t { debugp_param("defn/iseq", iseqval); + ADD_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CBASE)); ADD_INSN1(ret, line, putobject, ID2SYM(node->nd_mid)); diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c index 93cb2a5..a83b70d 100644 --- a/ext/coverage/coverage.c +++ b/ext/coverage/coverage.c @@ -38,8 +38,12 @@ VALUE path = (VALUE)key; VALUE coverage = (VALUE)val; VALUE coverages = (VALUE)h; - coverage = rb_ary_dup(coverage); - rb_ary_clear((VALUE)val); + VALUE line_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("lines"))); + line_coverage = rb_ary_dup(line_coverage); + coverage = rb_hash_dup(coverage); + rb_hash_clear((VALUE)val); + rb_hash_freeze(line_coverage); + rb_hash_aset(coverage, ID2SYM(rb_intern("lines")), line_coverage); rb_ary_freeze(coverage); rb_hash_aset(coverages, path, coverage); return ST_CONTINUE; diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index 6c6cee2..962a8d0 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1714,6 +1714,7 @@ struct RBignum { #define RUBY_EVENT_C_CALL 0x0020 #define RUBY_EVENT_C_RETURN 0x0040 #define RUBY_EVENT_RAISE 0x0080 +#define RUBY_EVENT_DEFN 0x0090 #define RUBY_EVENT_ALL 0x00ff /* for TracePoint extended events */ @@ -1726,6 +1727,7 @@ struct RBignum { /* special events */ #define RUBY_EVENT_SPECIFIED_LINE 0x010000 #define RUBY_EVENT_COVERAGE 0x020000 +#define RUBY_EVENT_MCOVERAGE 0x040000 /* internal events */ #define RUBY_INTERNAL_EVENT_SWITCH 0x040000 diff --git a/iseq.c b/iseq.c index 3b0c10b..0c2d0c4 100644 --- a/iseq.c +++ b/iseq.c @@ -113,6 +113,7 @@ RUBY_MARK_UNLESS_NULL((VALUE)iseq->cref_stack); RUBY_MARK_UNLESS_NULL(iseq->klass); RUBY_MARK_UNLESS_NULL(iseq->coverage); + RUBY_MARK_UNLESS_NULL(iseq->method_coverage); RUBY_MARK_UNLESS_NULL(iseq->orig); if (iseq->compile_data != 0) { @@ -305,11 +306,16 @@ iseq->compile_data->last_coverable_line = -1; RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); + RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); if (!GET_THREAD()->parse_in_eval) { VALUE coverages = rb_get_coverages(); if (RTEST(coverages)) { - RB_OBJ_WRITE(iseq->self, &iseq->coverage, rb_hash_lookup(coverages, path)); + RB_OBJ_WRITE(iseq->self, &iseq->coverage, + rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("lines")))); + RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, + rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("methods")))); if (NIL_P(iseq->coverage)) RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); + if (NIL_P(iseq->method_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); } } diff --git a/parse.y b/parse.y index 25946dc..067820f 100644 --- a/parse.y +++ b/parse.y @@ -5299,11 +5299,15 @@ coverage(VALUE fname, int n) VALUE coverages = rb_get_coverages(); if (RTEST(coverages) && RBASIC(coverages)->klass == 0) { VALUE lines = rb_ary_new2(n); + VALUE rb_file_coverage = rb_hash_new(); + VALUE methods = rb_hash_new(); int i; RBASIC_CLEAR_CLASS(lines); for (i = 0; i < n; i++) RARRAY_ASET(lines, i, Qnil); RARRAY(lines)->as.heap.len = n; - rb_hash_aset(coverages, fname, lines); + rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("lines")), lines); + rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("methods")), methods); + rb_hash_aset(coverages, fname, rb_file_coverage); return lines; } return 0; diff --git a/thread.c b/thread.c index 0e743c6..07fa2a0 100644 --- a/thread.c +++ b/thread.c @@ -3862,7 +3862,7 @@ struct select_args { clear_coverage_i(st_data_t key, st_data_t val, st_data_t dummy) { int i; - VALUE lines = (VALUE)val; + VALUE lines = rb_hash_lookup(val, ID2SYM(rb_intern("lines"))); for (i = 0; i < RARRAY_LEN(lines); i++) { if (RARRAY_AREF(lines, i) != Qnil) { @@ -5254,6 +5254,25 @@ struct exec_recursive_params { } } +static void +update_method_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) +{ + VALUE method_coverage = GET_THREAD()->cfp->iseq->method_coverage; + if (method_coverage) { + long line = rb_sourceline(); + VALUE info = rb_hash_lookup(method_coverage, LONG2FIX(line)); + + if (info == Qnil) { + VALUE info = rb_hash_new(); + rb_hash_aset(info, ID2SYM(rb_intern("count")), INT2FIX(1)); + rb_hash_aset(method_coverage, LONG2FIX(line), info); + } else { + long count = FIX2LONG(rb_hash_lookup(info, ID2SYM(rb_intern("count")))) + 1; + rb_hash_aset(info, ID2SYM(rb_intern("count")), LONG2FIX(count)); + } + } +} + VALUE rb_get_coverages(void) { @@ -5265,6 +5284,7 @@ struct exec_recursive_params { { GET_VM()->coverages = coverages; rb_add_event_hook(update_coverage, RUBY_EVENT_COVERAGE, Qnil); + rb_add_event_hook(update_method_coverage, RUBY_EVENT_MCOVERAGE, Qnil); } void diff --git a/vm_core.h b/vm_core.h index 08a09da..e2cc09c 100644 --- a/vm_core.h +++ b/vm_core.h @@ -226,7 +226,8 @@ struct rb_iseq_struct { VALUE *iseq_encoded; /* encoded iseq */ unsigned long iseq_size; const VALUE mark_ary; /* Array: includes operands which should be GC marked */ - const VALUE coverage; /* coverage array */ + const VALUE coverage; /* coverage array */ + const VALUE method_coverage; /* method coverage array */ /* insn info, must be freed */ struct iseq_line_info_entry *line_info_table; -- 1.8.5.5 From 32546ac526d12d442bcddb66a253323ce681bf98 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Mon, 20 Jan 2014 23:31:04 -0800 Subject: [PATCH 2/8] Move new code out from ADD_TRACE; only track call count for methods --- compile.c | 15 ++++++++------- thread.c | 10 ++-------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/compile.c b/compile.c index f563a0a..bf02f08 100644 --- a/compile.c +++ b/compile.c @@ -226,12 +226,6 @@ struct iseq_compile_data_ensure_node_stack { iseq->compile_data->last_coverable_line = (line); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_COVERAGE)); \ } \ - if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ - /*iseq->compile_data->last_coverable_line = (line); \ - VALUE info = rb_hash_new(); \ - rb_hash_aset(info, ID2SYM(rb_intern("count")), INT2FIX(0));*/ \ - rb_hash_aset(iseq->method_coverage, LONG2FIX(line), Qnil); \ - } \ if ((event) == RUBY_EVENT_CALL && iseq->method_coverage && \ (line) != iseq->compile_data->last_coverable_line) { \ iseq->compile_data->last_coverable_line = (line); \ @@ -242,6 +236,13 @@ struct iseq_compile_data_ensure_node_stack { } \ } while (0) +#define ADD_COVERAGE_TRACE(seq, line, event) \ + do { \ + if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ + rb_hash_aset(iseq->method_coverage, LONG2FIX(line), INT2FIX(0)); \ + } \ + } while (0) + /* add label */ #define ADD_LABEL(seq, label) \ ADD_ELEM((seq), (LINK_ELEMENT *) (label)) @@ -4949,7 +4950,7 @@ enum compile_array_type_t { debugp_param("defn/iseq", iseqval); - ADD_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); + ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CBASE)); ADD_INSN1(ret, line, putobject, ID2SYM(node->nd_mid)); diff --git a/thread.c b/thread.c index 07fa2a0..9a72d93 100644 --- a/thread.c +++ b/thread.c @@ -5262,14 +5262,8 @@ struct exec_recursive_params { long line = rb_sourceline(); VALUE info = rb_hash_lookup(method_coverage, LONG2FIX(line)); - if (info == Qnil) { - VALUE info = rb_hash_new(); - rb_hash_aset(info, ID2SYM(rb_intern("count")), INT2FIX(1)); - rb_hash_aset(method_coverage, LONG2FIX(line), info); - } else { - long count = FIX2LONG(rb_hash_lookup(info, ID2SYM(rb_intern("count")))) + 1; - rb_hash_aset(info, ID2SYM(rb_intern("count")), LONG2FIX(count)); - } + long count = FIX2LONG(info) + 1; + rb_hash_aset(method_coverage, LONG2FIX(line), LONG2FIX(count)); } } -- 1.8.5.5 From d34e7eb329f962f85c7de0fc552bfa1de64edbca Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Tue, 21 Jan 2014 11:46:35 -0800 Subject: [PATCH 3/8] Add support for branch-level coverage, for NODE_IF only --- compile.c | 8 ++++++++ include/ruby/ruby.h | 2 ++ iseq.c | 4 ++++ parse.y | 2 ++ thread.c | 28 ++++++++++++++++++++-------- vm_core.h | 1 + 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/compile.c b/compile.c index bf02f08..12781aa 100644 --- a/compile.c +++ b/compile.c @@ -241,6 +241,10 @@ struct iseq_compile_data_ensure_node_stack { if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ rb_hash_aset(iseq->method_coverage, LONG2FIX(line), INT2FIX(0)); \ } \ + if ((event) == RUBY_EVENT_BRANCH && iseq->branch_coverage) { \ + rb_hash_aset(iseq->branch_coverage, LONG2FIX(line), INT2FIX(0)); \ + ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_BCOVERAGE)); \ + } \ } while (0) /* add label */ @@ -3257,10 +3261,14 @@ enum compile_array_type_t { ADD_SEQ(ret, cond_seq); ADD_LABEL(ret, then_label); + if (node->nd_body) + ADD_COVERAGE_TRACE(ret, nd_line(node->nd_body), RUBY_EVENT_BRANCH); ADD_SEQ(ret, then_seq); ADD_INSNL(ret, line, jump, end_label); ADD_LABEL(ret, else_label); + if (node->nd_else) + ADD_COVERAGE_TRACE(ret, nd_line(node->nd_else), RUBY_EVENT_BRANCH); ADD_SEQ(ret, else_seq); ADD_LABEL(ret, end_label); diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index 962a8d0..e7e101b 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1715,6 +1715,7 @@ struct RBignum { #define RUBY_EVENT_C_RETURN 0x0040 #define RUBY_EVENT_RAISE 0x0080 #define RUBY_EVENT_DEFN 0x0090 +#define RUBY_EVENT_BRANCH 0x00a0 #define RUBY_EVENT_ALL 0x00ff /* for TracePoint extended events */ @@ -1728,6 +1729,7 @@ struct RBignum { #define RUBY_EVENT_SPECIFIED_LINE 0x010000 #define RUBY_EVENT_COVERAGE 0x020000 #define RUBY_EVENT_MCOVERAGE 0x040000 +#define RUBY_EVENT_BCOVERAGE 0x080000 /* internal events */ #define RUBY_INTERNAL_EVENT_SWITCH 0x040000 diff --git a/iseq.c b/iseq.c index 0c2d0c4..7f9828e 100644 --- a/iseq.c +++ b/iseq.c @@ -114,6 +114,7 @@ RUBY_MARK_UNLESS_NULL(iseq->klass); RUBY_MARK_UNLESS_NULL(iseq->coverage); RUBY_MARK_UNLESS_NULL(iseq->method_coverage); + RUBY_MARK_UNLESS_NULL(iseq->branch_coverage); RUBY_MARK_UNLESS_NULL(iseq->orig); if (iseq->compile_data != 0) { @@ -307,6 +308,7 @@ RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); + RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, Qfalse); if (!GET_THREAD()->parse_in_eval) { VALUE coverages = rb_get_coverages(); if (RTEST(coverages)) { @@ -314,6 +316,8 @@ rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("lines")))); RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("methods")))); + RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, + rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("branches")))); if (NIL_P(iseq->coverage)) RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); if (NIL_P(iseq->method_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); } diff --git a/parse.y b/parse.y index 067820f..d850ae7 100644 --- a/parse.y +++ b/parse.y @@ -5301,12 +5301,14 @@ coverage(VALUE fname, int n) VALUE lines = rb_ary_new2(n); VALUE rb_file_coverage = rb_hash_new(); VALUE methods = rb_hash_new(); + VALUE branches = rb_hash_new(); int i; RBASIC_CLEAR_CLASS(lines); for (i = 0; i < n; i++) RARRAY_ASET(lines, i, Qnil); RARRAY(lines)->as.heap.len = n; rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("lines")), lines); rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("methods")), methods); + rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("branches")), branches); rb_hash_aset(coverages, fname, rb_file_coverage); return lines; } diff --git a/thread.c b/thread.c index 9a72d93..cb5d169 100644 --- a/thread.c +++ b/thread.c @@ -5255,15 +5255,26 @@ struct exec_recursive_params { } static void -update_method_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) +update_detailed_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) { - VALUE method_coverage = GET_THREAD()->cfp->iseq->method_coverage; - if (method_coverage) { - long line = rb_sourceline(); - VALUE info = rb_hash_lookup(method_coverage, LONG2FIX(line)); + VALUE coverage; + if (event == RUBY_EVENT_MCOVERAGE) { + coverage = GET_THREAD()->cfp->iseq->method_coverage; + } else if (event == RUBY_EVENT_BCOVERAGE) { + coverage = GET_THREAD()->cfp->iseq->branch_coverage; + } else { + rb_raise(rb_eArgError, "unknown detailed coverage event"); + } + + if (coverage) { + VALUE line = LONG2FIX(rb_sourceline()); + VALUE count = rb_hash_lookup(coverage, line); + + if (count == Qnil) { + return; + } - long count = FIX2LONG(info) + 1; - rb_hash_aset(method_coverage, LONG2FIX(line), LONG2FIX(count)); + rb_hash_aset(coverage, line, LONG2FIX(FIX2LONG(count) + 1)); } } @@ -5278,7 +5289,8 @@ struct exec_recursive_params { { GET_VM()->coverages = coverages; rb_add_event_hook(update_coverage, RUBY_EVENT_COVERAGE, Qnil); - rb_add_event_hook(update_method_coverage, RUBY_EVENT_MCOVERAGE, Qnil); + rb_add_event_hook(update_detailed_coverage, RUBY_EVENT_MCOVERAGE, Qnil); + rb_add_event_hook(update_detailed_coverage, RUBY_EVENT_BCOVERAGE, Qnil); } void diff --git a/vm_core.h b/vm_core.h index e2cc09c..43de6c6 100644 --- a/vm_core.h +++ b/vm_core.h @@ -228,6 +228,7 @@ struct rb_iseq_struct { const VALUE mark_ary; /* Array: includes operands which should be GC marked */ const VALUE coverage; /* coverage array */ const VALUE method_coverage; /* method coverage array */ + const VALUE branch_coverage; /* branch coverage array */ /* insn info, must be freed */ struct iseq_line_info_entry *line_info_table; -- 1.8.5.5 From 2f17f0a3b040d1c18df526c7efbd11a7d609931b Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Tue, 21 Jan 2014 16:44:43 -0800 Subject: [PATCH 4/8] Test new method coverage and branch coverage metrics --- compile.c | 3 ++- ext/coverage/coverage.c | 21 ++++++++++++++-- iseq.c | 1 + test/coverage/test_branch_coverage.rb | 46 +++++++++++++++++++++++++++++++++++ test/coverage/test_coverage.rb | 13 +++++++--- test/coverage/test_method_coverage.rb | 36 +++++++++++++++++++++++++++ thread.c | 1 + 7 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 test/coverage/test_branch_coverage.rb create mode 100644 test/coverage/test_method_coverage.rb diff --git a/compile.c b/compile.c index 12781aa..b965b9e 100644 --- a/compile.c +++ b/compile.c @@ -3267,7 +3267,8 @@ enum compile_array_type_t { ADD_INSNL(ret, line, jump, end_label); ADD_LABEL(ret, else_label); - if (node->nd_else) + /* do not trace elsif node */ + if (node->nd_else && nd_type(node->nd_else) != NODE_IF) ADD_COVERAGE_TRACE(ret, nd_line(node->nd_else), RUBY_EVENT_BRANCH); ADD_SEQ(ret, else_seq); diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c index a83b70d..976a455 100644 --- a/ext/coverage/coverage.c +++ b/ext/coverage/coverage.c @@ -38,12 +38,29 @@ VALUE path = (VALUE)key; VALUE coverage = (VALUE)val; VALUE coverages = (VALUE)h; + VALUE line_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("lines"))); + VALUE method_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("methods"))); + VALUE branch_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("branches"))); + line_coverage = rb_ary_dup(line_coverage); + method_coverage = rb_hash_dup(method_coverage); + branch_coverage = rb_hash_dup(branch_coverage); + + rb_ary_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("lines")))); + rb_hash_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("methods")))); + rb_hash_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("branches")))); + coverage = rb_hash_dup(coverage); - rb_hash_clear((VALUE)val); - rb_hash_freeze(line_coverage); + + rb_ary_freeze(line_coverage); + rb_hash_freeze(method_coverage); + rb_hash_freeze(branch_coverage); + rb_hash_aset(coverage, ID2SYM(rb_intern("lines")), line_coverage); + rb_hash_aset(coverage, ID2SYM(rb_intern("methods")), method_coverage); + rb_hash_aset(coverage, ID2SYM(rb_intern("branches")), branch_coverage); + rb_ary_freeze(coverage); rb_hash_aset(coverages, path, coverage); return ST_CONTINUE; diff --git a/iseq.c b/iseq.c index 7f9828e..776610b 100644 --- a/iseq.c +++ b/iseq.c @@ -320,6 +320,7 @@ rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("branches")))); if (NIL_P(iseq->coverage)) RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); if (NIL_P(iseq->method_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); + if (NIL_P(iseq->branch_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, Qfalse); } } diff --git a/test/coverage/test_branch_coverage.rb b/test/coverage/test_branch_coverage.rb new file mode 100644 index 0000000..7c05c68 --- /dev/null +++ b/test/coverage/test_branch_coverage.rb @@ -0,0 +1,46 @@ +require "test/unit" +require "coverage" +require "tmpdir" + +class TestBranchCoverage < Test::Unit::TestCase + def test_branch_coverage + loaded_features = $".dup + + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + if 2+2 == 4 + :ok + end + + :ok unless [].size > 0 + :bad unless [].size == 0 + + ary = [1,2,3] + if ary.include? 4 + :bad + elsif ary.include? 5 + :also_bad + else + :good + end + EOS + end + + Coverage.start + require tmp + '/test.rb' + branch_coverage = Coverage.result[tmp + '/test.rb'][:branches] + assert_equal 6, branch_coverage.size + assert_equal 1, branch_coverage[2] + assert_equal 1, branch_coverage[5] + assert_equal 0, branch_coverage[6] + assert_equal 0, branch_coverage[10] + assert_equal 0, branch_coverage[12] + assert_equal 1, branch_coverage[14] + } + } + ensure + $".replace loaded_features + end +end diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb index f4f192a..25c6ee7 100644 --- a/test/coverage/test_coverage.rb +++ b/test/coverage/test_coverage.rb @@ -6,13 +6,18 @@ class TestCoverage < Test::Unit::TestCase def test_result_without_start assert_raise(RuntimeError) {Coverage.result} end + def test_result_with_nothing Coverage.start result = Coverage.result assert_kind_of(Hash, result) + assert(! result.empty?) result.each do |key, val| assert_kind_of(String, key) - assert_kind_of(Array, val) + assert_kind_of(Hash, val) + assert_kind_of(Array, val[:lines]) + assert_kind_of(Hash, val[:methods]) + assert_kind_of(Hash, val[:branches]) end end @@ -31,10 +36,10 @@ def coverage_test_method Coverage.start require tmp + '/test.rb' - assert_equal 3, Coverage.result[tmp + '/test.rb'].size + assert_equal 3, Coverage.result[tmp + '/test.rb'][:lines].size Coverage.start coverage_test_method - assert_equal 0, Coverage.result[tmp + '/test.rb'].size + assert_equal 0, Coverage.result[tmp + '/test.rb'][:lines].size } } ensure @@ -55,7 +60,7 @@ def test_big_code Coverage.start require tmp + '/test.rb' - assert_equal 10003, Coverage.result[tmp + '/test.rb'].size + assert_equal 10003, Coverage.result[tmp + '/test.rb'][:lines].size } } ensure diff --git a/test/coverage/test_method_coverage.rb b/test/coverage/test_method_coverage.rb new file mode 100644 index 0000000..b7d9641 --- /dev/null +++ b/test/coverage/test_method_coverage.rb @@ -0,0 +1,36 @@ +require "test/unit" +require "coverage" +require "tmpdir" + +class TestMethodCoverage < Test::Unit::TestCase + def test_method_coverage + loaded_features = $".dup + + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + def method_one + :one + end + + def method_two + :two + end + method_two; method_two + EOS + end + + Coverage.start + require tmp + '/test.rb' + method_coverage = Coverage.result[tmp + '/test.rb'][:methods] + + assert_equal 2, method_coverage.size + assert_equal 0, method_coverage[1] + assert_equal 2, method_coverage[5] + } + } + ensure + $".replace loaded_features + end +end diff --git a/thread.c b/thread.c index cb5d169..0b2cd52 100644 --- a/thread.c +++ b/thread.c @@ -5298,6 +5298,7 @@ struct exec_recursive_params { { GET_VM()->coverages = Qfalse; rb_remove_event_hook(update_coverage); + rb_remove_event_hook(update_detailed_coverage); } VALUE -- 1.8.5.5 From 5fd0f29630c5f05fedc2fd1c8877440a5e897a61 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 24 Jan 2014 21:52:00 -0800 Subject: [PATCH 5/8] Track branch coverage for case and caseless NODE_WHENs, as well as else nodes following NODE_WHENs --- compile.c | 7 ++++++ test/coverage/test_branch_coverage.rb | 43 ++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/compile.c b/compile.c index b965b9e..96d8c8c 100644 --- a/compile.c +++ b/compile.c @@ -3316,6 +3316,8 @@ enum compile_array_type_t { l1 = NEW_LABEL(line); ADD_LABEL(body_seq, l1); + if (node->nd_body) + ADD_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); ADD_INSN(body_seq, line, pop); COMPILE_(body_seq, "when body", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3354,6 +3356,7 @@ enum compile_array_type_t { /* else */ if (node) { ADD_LABEL(cond_seq, elselabel); + ADD_COVERAGE_TRACE(cond_seq, nd_line(node), RUBY_EVENT_BRANCH); ADD_INSN(cond_seq, line, pop); COMPILE_(cond_seq, "else", node, poped); ADD_INSNL(cond_seq, line, jump, endlabel); @@ -3393,6 +3396,8 @@ enum compile_array_type_t { while (node && nd_type(node) == NODE_WHEN) { LABEL *l1 = NEW_LABEL(line = nd_line(node)); ADD_LABEL(body_seq, l1); + if (node->nd_body) + ADD_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); COMPILE_(body_seq, "when", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3424,6 +3429,7 @@ enum compile_array_type_t { node = node->nd_next; } /* else */ + ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_BRANCH); COMPILE_(ret, "else", node, poped); ADD_INSNL(ret, nd_line(orig_node), jump, endlabel); @@ -4980,6 +4986,7 @@ enum compile_array_type_t { debugp_param("defs/iseq", iseqval); + ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); COMPILE(ret, "defs: recv", node->nd_recv); ADD_INSN1(ret, line, putobject, ID2SYM(node->nd_mid)); diff --git a/test/coverage/test_branch_coverage.rb b/test/coverage/test_branch_coverage.rb index 7c05c68..a126a89 100644 --- a/test/coverage/test_branch_coverage.rb +++ b/test/coverage/test_branch_coverage.rb @@ -3,7 +3,7 @@ require "tmpdir" class TestBranchCoverage < Test::Unit::TestCase - def test_branch_coverage + def test_if_else_coverage loaded_features = $".dup Dir.mktmpdir {|tmp| @@ -43,4 +43,45 @@ def test_branch_coverage ensure $".replace loaded_features end + + def test_when_coverage + loaded_features = $".dup + + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + case + when 2 + 2 == 5 + x = :bad + x = :math + when 2 + 2 == 4 + x = :good + else + x = :also_bad + end + + case [1,2,3] + when String + :string? + else + :else + end + EOS + end + + Coverage.start + require tmp + '/test.rb' + branch_coverage = Coverage.result[tmp + '/test.rb'][:branches] + assert_equal 5, branch_coverage.size + assert_equal 0, branch_coverage[3] + assert_equal 1, branch_coverage[6] + assert_equal 0, branch_coverage[8] + assert_equal 0, branch_coverage[13] + assert_equal 1, branch_coverage[15] + } + } + ensure + $".replace loaded_features + end end -- 1.8.5.5 From 471d0a8a94e9395c7a3ee6c89d3b72ec920cc9d9 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 20 Feb 2014 19:54:42 -0800 Subject: [PATCH 6/8] Split out ADD_[METHOD,BRANCH]_COVERAGE_TRACE macros, and remove RUBY_EVENT_MCOVERAGE instruction from ADD_TRACE --- compile.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/compile.c b/compile.c index 96d8c8c..ef8c0ca 100644 --- a/compile.c +++ b/compile.c @@ -226,21 +226,23 @@ struct iseq_compile_data_ensure_node_stack { iseq->compile_data->last_coverable_line = (line); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_COVERAGE)); \ } \ - if ((event) == RUBY_EVENT_CALL && iseq->method_coverage && \ - (line) != iseq->compile_data->last_coverable_line) { \ - iseq->compile_data->last_coverable_line = (line); \ - ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_MCOVERAGE)); \ - } \ if (iseq->compile_data->option->trace_instruction) { \ ADD_INSN1((seq), (line), trace, INT2FIX(event)); \ } \ } while (0) -#define ADD_COVERAGE_TRACE(seq, line, event) \ +#define ADD_METHOD_COVERAGE_TRACE(seq, line, event, end_line) \ do { \ if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ rb_hash_aset(iseq->method_coverage, LONG2FIX(line), INT2FIX(0)); \ } \ + if ((event) == RUBY_EVENT_CALL && iseq->method_coverage) { \ + ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_MCOVERAGE)); \ + } \ + } while (0) + +#define ADD_BRANCH_COVERAGE_TRACE(seq, line, event) \ + do { \ if ((event) == RUBY_EVENT_BRANCH && iseq->branch_coverage) { \ rb_hash_aset(iseq->branch_coverage, LONG2FIX(line), INT2FIX(0)); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_BCOVERAGE)); \ @@ -515,6 +517,7 @@ struct iseq_compile_data_ensure_node_stack { case ISEQ_TYPE_METHOD: { ADD_TRACE(ret, FIX2INT(iseq->location.first_lineno), RUBY_EVENT_CALL); + ADD_METHOD_COVERAGE_TRACE(ret, FIX2INT(iseq->location.first_lineno), RUBY_EVENT_CALL, nd_line(node)); COMPILE(ret, "scoped node", node->nd_body); ADD_TRACE(ret, nd_line(node), RUBY_EVENT_RETURN); break; @@ -3262,14 +3265,14 @@ enum compile_array_type_t { ADD_LABEL(ret, then_label); if (node->nd_body) - ADD_COVERAGE_TRACE(ret, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node->nd_body), RUBY_EVENT_BRANCH); ADD_SEQ(ret, then_seq); ADD_INSNL(ret, line, jump, end_label); ADD_LABEL(ret, else_label); /* do not trace elsif node */ if (node->nd_else && nd_type(node->nd_else) != NODE_IF) - ADD_COVERAGE_TRACE(ret, nd_line(node->nd_else), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node->nd_else), RUBY_EVENT_BRANCH); ADD_SEQ(ret, else_seq); ADD_LABEL(ret, end_label); @@ -3317,7 +3320,7 @@ enum compile_array_type_t { l1 = NEW_LABEL(line); ADD_LABEL(body_seq, l1); if (node->nd_body) - ADD_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); ADD_INSN(body_seq, line, pop); COMPILE_(body_seq, "when body", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3356,7 +3359,7 @@ enum compile_array_type_t { /* else */ if (node) { ADD_LABEL(cond_seq, elselabel); - ADD_COVERAGE_TRACE(cond_seq, nd_line(node), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(cond_seq, nd_line(node), RUBY_EVENT_BRANCH); ADD_INSN(cond_seq, line, pop); COMPILE_(cond_seq, "else", node, poped); ADD_INSNL(cond_seq, line, jump, endlabel); @@ -3397,7 +3400,7 @@ enum compile_array_type_t { LABEL *l1 = NEW_LABEL(line = nd_line(node)); ADD_LABEL(body_seq, l1); if (node->nd_body) - ADD_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); COMPILE_(body_seq, "when", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3429,7 +3432,7 @@ enum compile_array_type_t { node = node->nd_next; } /* else */ - ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_BRANCH); + ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_BRANCH); COMPILE_(ret, "else", node, poped); ADD_INSNL(ret, nd_line(orig_node), jump, endlabel); @@ -4965,7 +4968,7 @@ enum compile_array_type_t { debugp_param("defn/iseq", iseqval); - ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); + ADD_METHOD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN, nd_line(node->nd_defn)); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CBASE)); ADD_INSN1(ret, line, putobject, ID2SYM(node->nd_mid)); @@ -4986,7 +4989,7 @@ enum compile_array_type_t { debugp_param("defs/iseq", iseqval); - ADD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN); + ADD_METHOD_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DEFN, nd_line(node->nd_defn)); ADD_INSN1(ret, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); COMPILE(ret, "defs: recv", node->nd_recv); ADD_INSN1(ret, line, putobject, ID2SYM(node->nd_mid)); -- 1.8.5.5 From cc50eab44f5ce0a4febdc05bdd99a09708e78b7e Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 20 Feb 2014 20:55:24 -0800 Subject: [PATCH 7/8] Convert iseq->coverage to a struct containing line, method, and branch coverage data --- compile.c | 14 +++++++------- iseq.c | 29 +++++++++++++++++------------ thread.c | 6 +++--- vm_core.h | 10 +++++++--- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/compile.c b/compile.c index ef8c0ca..8023f7a 100644 --- a/compile.c +++ b/compile.c @@ -220,9 +220,9 @@ struct iseq_compile_data_ensure_node_stack { #define ADD_TRACE(seq, line, event) \ do { \ - if ((event) == RUBY_EVENT_LINE && iseq->coverage && \ + if ((event) == RUBY_EVENT_LINE && iseq->coverage->lines && \ (line) != iseq->compile_data->last_coverable_line) { \ - RARRAY_ASET(iseq->coverage, (line) - 1, INT2FIX(0)); \ + RARRAY_ASET(iseq->coverage->lines, (line) - 1, INT2FIX(0)); \ iseq->compile_data->last_coverable_line = (line); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_COVERAGE)); \ } \ @@ -233,18 +233,18 @@ struct iseq_compile_data_ensure_node_stack { #define ADD_METHOD_COVERAGE_TRACE(seq, line, event, end_line) \ do { \ - if ((event) == RUBY_EVENT_DEFN && iseq->method_coverage) { \ - rb_hash_aset(iseq->method_coverage, LONG2FIX(line), INT2FIX(0)); \ + if ((event) == RUBY_EVENT_DEFN && iseq->coverage->methods) { \ + rb_hash_aset(iseq->coverage->methods, LONG2FIX(line), INT2FIX(0)); \ } \ - if ((event) == RUBY_EVENT_CALL && iseq->method_coverage) { \ + if ((event) == RUBY_EVENT_CALL && iseq->coverage->methods) { \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_MCOVERAGE)); \ } \ } while (0) #define ADD_BRANCH_COVERAGE_TRACE(seq, line, event) \ do { \ - if ((event) == RUBY_EVENT_BRANCH && iseq->branch_coverage) { \ - rb_hash_aset(iseq->branch_coverage, LONG2FIX(line), INT2FIX(0)); \ + if ((event) == RUBY_EVENT_BRANCH && iseq->coverage->branches) { \ + rb_hash_aset(iseq->coverage->branches, LONG2FIX(line), INT2FIX(0)); \ ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_BCOVERAGE)); \ } \ } while (0) diff --git a/iseq.c b/iseq.c index 776610b..ede15a8 100644 --- a/iseq.c +++ b/iseq.c @@ -112,11 +112,15 @@ RUBY_MARK_UNLESS_NULL((VALUE)iseq->cref_stack); RUBY_MARK_UNLESS_NULL(iseq->klass); - RUBY_MARK_UNLESS_NULL(iseq->coverage); - RUBY_MARK_UNLESS_NULL(iseq->method_coverage); - RUBY_MARK_UNLESS_NULL(iseq->branch_coverage); RUBY_MARK_UNLESS_NULL(iseq->orig); + if (iseq->coverage != 0) { + struct rb_coverage_struct *const coverage = iseq->coverage; + RUBY_MARK_UNLESS_NULL(coverage->lines); + RUBY_MARK_UNLESS_NULL(coverage->methods); + RUBY_MARK_UNLESS_NULL(coverage->branches); + } + if (iseq->compile_data != 0) { struct iseq_compile_data *const compile_data = iseq->compile_data; RUBY_MARK_UNLESS_NULL(compile_data->mark_ary); @@ -306,21 +310,22 @@ iseq->compile_data->option = option; iseq->compile_data->last_coverable_line = -1; - RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); - RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); - RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, Qfalse); + iseq->coverage = ALLOC(struct rb_coverage_struct); + RB_OBJ_WRITE(iseq->self, &iseq->coverage->lines, Qfalse); + RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, Qfalse); + RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, Qfalse); if (!GET_THREAD()->parse_in_eval) { VALUE coverages = rb_get_coverages(); if (RTEST(coverages)) { - RB_OBJ_WRITE(iseq->self, &iseq->coverage, + RB_OBJ_WRITE(iseq->self, &iseq->coverage->lines, rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("lines")))); - RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, + RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("methods")))); - RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, + RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("branches")))); - if (NIL_P(iseq->coverage)) RB_OBJ_WRITE(iseq->self, &iseq->coverage, Qfalse); - if (NIL_P(iseq->method_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->method_coverage, Qfalse); - if (NIL_P(iseq->branch_coverage)) RB_OBJ_WRITE(iseq->self, &iseq->branch_coverage, Qfalse); + if (NIL_P(iseq->coverage->lines)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->lines, Qfalse); + if (NIL_P(iseq->coverage->methods)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, Qfalse); + if (NIL_P(iseq->coverage->branches)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, Qfalse); } } diff --git a/thread.c b/thread.c index 0b2cd52..b395cd6 100644 --- a/thread.c +++ b/thread.c @@ -5240,7 +5240,7 @@ struct exec_recursive_params { static void update_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) { - VALUE coverage = GET_THREAD()->cfp->iseq->coverage; + VALUE coverage = GET_THREAD()->cfp->iseq->coverage->lines; if (coverage && RBASIC(coverage)->klass == 0) { long line = rb_sourceline() - 1; long count; @@ -5259,9 +5259,9 @@ struct exec_recursive_params { { VALUE coverage; if (event == RUBY_EVENT_MCOVERAGE) { - coverage = GET_THREAD()->cfp->iseq->method_coverage; + coverage = GET_THREAD()->cfp->iseq->coverage->methods; } else if (event == RUBY_EVENT_BCOVERAGE) { - coverage = GET_THREAD()->cfp->iseq->branch_coverage; + coverage = GET_THREAD()->cfp->iseq->coverage->branches; } else { rb_raise(rb_eArgError, "unknown detailed coverage event"); } diff --git a/vm_core.h b/vm_core.h index 43de6c6..b9e176b 100644 --- a/vm_core.h +++ b/vm_core.h @@ -201,6 +201,12 @@ struct iseq_inline_cache_entry { size_t first_lineno; } rb_iseq_location_t; +struct rb_coverage_struct { + const VALUE lines; + const VALUE methods; + const VALUE branches; +}; + struct rb_iseq_struct; struct rb_iseq_struct { @@ -226,9 +232,7 @@ struct rb_iseq_struct { VALUE *iseq_encoded; /* encoded iseq */ unsigned long iseq_size; const VALUE mark_ary; /* Array: includes operands which should be GC marked */ - const VALUE coverage; /* coverage array */ - const VALUE method_coverage; /* method coverage array */ - const VALUE branch_coverage; /* branch coverage array */ + struct rb_coverage_struct *coverage; /* insn info, must be freed */ struct iseq_line_info_entry *line_info_table; -- 1.8.5.5 From 199368bff44f3ce92188c0c2ce5cbbdabee1ed5b Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Tue, 25 Feb 2014 18:54:04 -0800 Subject: [PATCH 8/8] Change branch coverage to much better defined decision coverage --- compile.c | 33 ++++---- ext/coverage/coverage.c | 10 +-- include/ruby/ruby.h | 6 +- iseq.c | 10 +-- parse.y | 4 +- test/coverage/test_branch_coverage.rb | 139 +++++++++++++++++++++++++++------- test/coverage/test_coverage.rb | 2 +- thread.c | 46 ++++++++--- vm_core.h | 2 +- 9 files changed, 181 insertions(+), 71 deletions(-) diff --git a/compile.c b/compile.c index 1e09c4a..486a4d0 100644 --- a/compile.c +++ b/compile.c @@ -241,11 +241,15 @@ struct iseq_compile_data_ensure_node_stack { } \ } while (0) -#define ADD_BRANCH_COVERAGE_TRACE(seq, line, event) \ +#define ADD_DECISION_COVERAGE_TRACE(seq, line, event) \ do { \ - if ((event) == RUBY_EVENT_BRANCH && iseq->coverage->branches) { \ - rb_hash_aset(iseq->coverage->branches, LONG2FIX(line), INT2FIX(0)); \ - ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_BCOVERAGE)); \ + if (((event) == RUBY_EVENT_DECISION_TRUE || (event) == RUBY_EVENT_DECISION_FALSE) \ + && iseq->coverage->decisions) { \ + rb_hash_aset(iseq->coverage->decisions, LONG2FIX(line), rb_assoc_new(INT2FIX(0), INT2FIX(0))); \ + if ((event) == RUBY_EVENT_DECISION_TRUE) \ + ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_DCOVERAGE_TRUE)); \ + if ((event) == RUBY_EVENT_DECISION_FALSE) \ + ADD_INSN1((seq), (line), trace, INT2FIX(RUBY_EVENT_DCOVERAGE_FALSE)); \ } \ } while (0) @@ -2576,6 +2580,7 @@ enum compile_array_type_t { ADD_INSN1(cond_seq, nd_line(vals), checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_CASE)); ADD_INSNL(cond_seq, nd_line(val), branchif, l1); + ADD_DECISION_COVERAGE_TRACE(cond_seq, nd_line(vals), RUBY_EVENT_DECISION_FALSE); vals = vals->nd_next; } return only_special_literals; @@ -3264,15 +3269,12 @@ enum compile_array_type_t { ADD_SEQ(ret, cond_seq); ADD_LABEL(ret, then_label); - if (node->nd_body) - ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + ADD_DECISION_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DECISION_TRUE); ADD_SEQ(ret, then_seq); ADD_INSNL(ret, line, jump, end_label); ADD_LABEL(ret, else_label); - /* do not trace elsif node */ - if (node->nd_else && nd_type(node->nd_else) != NODE_IF) - ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node->nd_else), RUBY_EVENT_BRANCH); + ADD_DECISION_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_DECISION_FALSE); ADD_SEQ(ret, else_seq); ADD_LABEL(ret, end_label); @@ -3319,8 +3321,8 @@ enum compile_array_type_t { l1 = NEW_LABEL(line); ADD_LABEL(body_seq, l1); - if (node->nd_body) - ADD_BRANCH_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + if (node->nd_head) + ADD_DECISION_COVERAGE_TRACE(body_seq, nd_line(node->nd_head), RUBY_EVENT_DECISION_TRUE); ADD_INSN(body_seq, line, pop); COMPILE_(body_seq, "when body", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3339,6 +3341,7 @@ enum compile_array_type_t { COMPILE(cond_seq, "when/cond splat", vals); ADD_INSN1(cond_seq, nd_line(vals), checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_CASE | VM_CHECKMATCH_ARRAY)); ADD_INSNL(cond_seq, nd_line(vals), branchif, l1); + ADD_DECISION_COVERAGE_TRACE(cond_seq, nd_line(vals), RUBY_EVENT_DECISION_FALSE); break; default: rb_bug("NODE_CASE: unknown node (%s)", @@ -3359,7 +3362,6 @@ enum compile_array_type_t { /* else */ if (node) { ADD_LABEL(cond_seq, elselabel); - ADD_BRANCH_COVERAGE_TRACE(cond_seq, nd_line(node), RUBY_EVENT_BRANCH); ADD_INSN(cond_seq, line, pop); COMPILE_(cond_seq, "else", node, poped); ADD_INSNL(cond_seq, line, jump, endlabel); @@ -3399,8 +3401,8 @@ enum compile_array_type_t { while (node && nd_type(node) == NODE_WHEN) { LABEL *l1 = NEW_LABEL(line = nd_line(node)); ADD_LABEL(body_seq, l1); - if (node->nd_body) - ADD_BRANCH_COVERAGE_TRACE(body_seq, nd_line(node->nd_body), RUBY_EVENT_BRANCH); + if (node->nd_head) + ADD_DECISION_COVERAGE_TRACE(body_seq, nd_line(node->nd_head), RUBY_EVENT_DECISION_TRUE); COMPILE_(body_seq, "when", node->nd_body, poped); ADD_INSNL(body_seq, line, jump, endlabel); @@ -3414,6 +3416,7 @@ enum compile_array_type_t { val = vals->nd_head; COMPILE(ret, "when2", val); ADD_INSNL(ret, nd_line(val), branchif, l1); + ADD_DECISION_COVERAGE_TRACE(ret, nd_line(vals), RUBY_EVENT_DECISION_FALSE); vals = vals->nd_next; } break; @@ -3424,6 +3427,7 @@ enum compile_array_type_t { COMPILE(ret, "when2/cond splat", vals); ADD_INSN1(ret, nd_line(vals), checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_WHEN | VM_CHECKMATCH_ARRAY)); ADD_INSNL(ret, nd_line(vals), branchif, l1); + ADD_DECISION_COVERAGE_TRACE(ret, nd_line(vals), RUBY_EVENT_DECISION_FALSE); break; default: rb_bug("NODE_WHEN: unknown node (%s)", @@ -3432,7 +3436,6 @@ enum compile_array_type_t { node = node->nd_next; } /* else */ - ADD_BRANCH_COVERAGE_TRACE(ret, nd_line(node), RUBY_EVENT_BRANCH); COMPILE_(ret, "else", node, poped); ADD_INSNL(ret, nd_line(orig_node), jump, endlabel); diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c index 976a455..fa7777c 100644 --- a/ext/coverage/coverage.c +++ b/ext/coverage/coverage.c @@ -41,25 +41,25 @@ VALUE line_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("lines"))); VALUE method_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("methods"))); - VALUE branch_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("branches"))); + VALUE decision_coverage = rb_hash_lookup(coverage, ID2SYM(rb_intern("decisions"))); line_coverage = rb_ary_dup(line_coverage); method_coverage = rb_hash_dup(method_coverage); - branch_coverage = rb_hash_dup(branch_coverage); + decision_coverage = rb_hash_dup(decision_coverage); rb_ary_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("lines")))); rb_hash_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("methods")))); - rb_hash_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("branches")))); + rb_hash_clear(rb_hash_lookup(coverage, ID2SYM(rb_intern("decisions")))); coverage = rb_hash_dup(coverage); rb_ary_freeze(line_coverage); rb_hash_freeze(method_coverage); - rb_hash_freeze(branch_coverage); + rb_hash_freeze(decision_coverage); rb_hash_aset(coverage, ID2SYM(rb_intern("lines")), line_coverage); rb_hash_aset(coverage, ID2SYM(rb_intern("methods")), method_coverage); - rb_hash_aset(coverage, ID2SYM(rb_intern("branches")), branch_coverage); + rb_hash_aset(coverage, ID2SYM(rb_intern("decisions")), decision_coverage); rb_ary_freeze(coverage); rb_hash_aset(coverages, path, coverage); diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index 31f0697..04c9346 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1677,7 +1677,8 @@ struct RStruct { #define RUBY_EVENT_C_RETURN 0x0040 #define RUBY_EVENT_RAISE 0x0080 #define RUBY_EVENT_DEFN 0x0090 -#define RUBY_EVENT_BRANCH 0x00a0 +#define RUBY_EVENT_DECISION_TRUE 0x00a0 +#define RUBY_EVENT_DECISION_FALSE 0x00b0 #define RUBY_EVENT_ALL 0x00ff /* for TracePoint extended events */ @@ -1691,7 +1692,8 @@ struct RStruct { #define RUBY_EVENT_SPECIFIED_LINE 0x010000 #define RUBY_EVENT_COVERAGE 0x020000 #define RUBY_EVENT_MCOVERAGE 0x040000 -#define RUBY_EVENT_BCOVERAGE 0x080000 +#define RUBY_EVENT_DCOVERAGE_TRUE 0x080000 +#define RUBY_EVENT_DCOVERAGE_FALSE 0x2000000 /* internal events */ #define RUBY_INTERNAL_EVENT_SWITCH 0x040000 diff --git a/iseq.c b/iseq.c index ede15a8..048f45c 100644 --- a/iseq.c +++ b/iseq.c @@ -118,7 +118,7 @@ struct rb_coverage_struct *const coverage = iseq->coverage; RUBY_MARK_UNLESS_NULL(coverage->lines); RUBY_MARK_UNLESS_NULL(coverage->methods); - RUBY_MARK_UNLESS_NULL(coverage->branches); + RUBY_MARK_UNLESS_NULL(coverage->decisions); } if (iseq->compile_data != 0) { @@ -313,7 +313,7 @@ iseq->coverage = ALLOC(struct rb_coverage_struct); RB_OBJ_WRITE(iseq->self, &iseq->coverage->lines, Qfalse); RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, Qfalse); - RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, Qfalse); + RB_OBJ_WRITE(iseq->self, &iseq->coverage->decisions, Qfalse); if (!GET_THREAD()->parse_in_eval) { VALUE coverages = rb_get_coverages(); if (RTEST(coverages)) { @@ -321,11 +321,11 @@ rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("lines")))); RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("methods")))); - RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, - rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("branches")))); + RB_OBJ_WRITE(iseq->self, &iseq->coverage->decisions, + rb_hash_lookup(rb_hash_lookup(coverages, path), ID2SYM(rb_intern("decisions")))); if (NIL_P(iseq->coverage->lines)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->lines, Qfalse); if (NIL_P(iseq->coverage->methods)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->methods, Qfalse); - if (NIL_P(iseq->coverage->branches)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->branches, Qfalse); + if (NIL_P(iseq->coverage->decisions)) RB_OBJ_WRITE(iseq->self, &iseq->coverage->decisions, Qfalse); } } diff --git a/parse.y b/parse.y index 5f80866..0c83ac9 100644 --- a/parse.y +++ b/parse.y @@ -5301,14 +5301,14 @@ coverage(VALUE fname, int n) VALUE lines = rb_ary_new2(n); VALUE rb_file_coverage = rb_hash_new(); VALUE methods = rb_hash_new(); - VALUE branches = rb_hash_new(); + VALUE decisions = rb_hash_new(); int i; RBASIC_CLEAR_CLASS(lines); for (i = 0; i < n; i++) RARRAY_ASET(lines, i, Qnil); RARRAY(lines)->as.heap.len = n; rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("lines")), lines); rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("methods")), methods); - rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("branches")), branches); + rb_hash_aset(rb_file_coverage, ID2SYM(rb_intern("decisions")), decisions); rb_hash_aset(coverages, fname, rb_file_coverage); return lines; } diff --git a/test/coverage/test_branch_coverage.rb b/test/coverage/test_branch_coverage.rb index a126a89..30163b5 100644 --- a/test/coverage/test_branch_coverage.rb +++ b/test/coverage/test_branch_coverage.rb @@ -30,14 +30,13 @@ def test_if_else_coverage Coverage.start require tmp + '/test.rb' - branch_coverage = Coverage.result[tmp + '/test.rb'][:branches] - assert_equal 6, branch_coverage.size - assert_equal 1, branch_coverage[2] - assert_equal 1, branch_coverage[5] - assert_equal 0, branch_coverage[6] - assert_equal 0, branch_coverage[10] - assert_equal 0, branch_coverage[12] - assert_equal 1, branch_coverage[14] + decision_coverage = Coverage.result[tmp + '/test.rb'][:decisions] + assert_equal 5, decision_coverage.size + assert_equal [1,0], decision_coverage[1] + assert_equal [0,1], decision_coverage[5] + assert_equal [1,0], decision_coverage[6] + assert_equal [0,1], decision_coverage[9] + assert_equal [0,1], decision_coverage[11] } } ensure @@ -51,34 +50,118 @@ def test_when_coverage Dir.chdir(tmp) { File.open("test.rb", "w") do |f| f.puts <<-EOS - case - when 2 + 2 == 5 - x = :bad - x = :math - when 2 + 2 == 4 - x = :good - else - x = :also_bad + def foo(arg) + case + when arg == 1 + x = :one + when arg == 2 + x = :two + else + x = :none + end end + foo(2); foo(2); foo(3) - case [1,2,3] - when String - :string? - else - :else + def bar(arg) + case arg + when String + :string? + when 1 + :one + else + :else + end + end + bar("BAR"); bar("BAZ"); bar(1); bar(7) + EOS + end + + Coverage.start + require tmp + '/test.rb' + decision_coverage = Coverage.result[tmp + '/test.rb'][:decisions] + assert_equal 4, decision_coverage.size + assert_equal [0,3], decision_coverage[3] + assert_equal [2,1], decision_coverage[5] + assert_equal [2,2], decision_coverage[15] + assert_equal [1,1], decision_coverage[17] + } + } + ensure + $".replace loaded_features + end + + def test_when_without_else_coverage + loaded_features = $".dup + + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + def foo(arg) + case + when arg == 1 + x = :one + when arg == 2 + x = :two + end + end + foo(2); foo(2); foo(3) + + def bar(arg) + case arg + when String + :string? + when 1 + :one + end end + bar("BAR"); bar("BAZ"); bar(1); bar(7) + EOS + end + + Coverage.start + require tmp + '/test.rb' + decision_coverage = Coverage.result[tmp + '/test.rb'][:decisions] + assert_equal 4, decision_coverage.size + assert_equal [0,3], decision_coverage[3] + assert_equal [2,1], decision_coverage[5] + assert_equal [2,2], decision_coverage[13] + assert_equal [1,1], decision_coverage[15] + } + } + ensure + $".replace loaded_features + end + + def test_when_with_splats_coverage + loaded_features = $".dup + + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + def prime?(arg) + primes = [2,3,5,7] + composites = [4,6,8,9] + + case arg + when *primes + :prime + when *composites + :composite + end + end + + 9.times {|i| prime?(i+1) } EOS end Coverage.start require tmp + '/test.rb' - branch_coverage = Coverage.result[tmp + '/test.rb'][:branches] - assert_equal 5, branch_coverage.size - assert_equal 0, branch_coverage[3] - assert_equal 1, branch_coverage[6] - assert_equal 0, branch_coverage[8] - assert_equal 0, branch_coverage[13] - assert_equal 1, branch_coverage[15] + decision_coverage = Coverage.result[tmp + '/test.rb'][:decisions] + assert_equal 2, decision_coverage.size + assert_equal [4,5], decision_coverage[6] + assert_equal [4,1], decision_coverage[8] } } ensure diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb index 25c6ee7..f93dc6c 100644 --- a/test/coverage/test_coverage.rb +++ b/test/coverage/test_coverage.rb @@ -17,7 +17,7 @@ def test_result_with_nothing assert_kind_of(Hash, val) assert_kind_of(Array, val[:lines]) assert_kind_of(Hash, val[:methods]) - assert_kind_of(Hash, val[:branches]) + assert_kind_of(Hash, val[:decisions]) end end diff --git a/thread.c b/thread.c index f665fa6..fdb0453 100644 --- a/thread.c +++ b/thread.c @@ -5135,16 +5135,9 @@ struct exec_recursive_params { } static void -update_detailed_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) +update_method_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) { - VALUE coverage; - if (event == RUBY_EVENT_MCOVERAGE) { - coverage = GET_THREAD()->cfp->iseq->coverage->methods; - } else if (event == RUBY_EVENT_BCOVERAGE) { - coverage = GET_THREAD()->cfp->iseq->coverage->branches; - } else { - rb_raise(rb_eArgError, "unknown detailed coverage event"); - } + VALUE coverage = GET_THREAD()->cfp->iseq->coverage->methods; if (coverage) { VALUE line = LONG2FIX(rb_sourceline()); @@ -5156,6 +5149,33 @@ struct exec_recursive_params { rb_hash_aset(coverage, line, LONG2FIX(FIX2LONG(count) + 1)); } + +} + +static void +update_decision_coverage(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass) +{ + struct rb_coverage_struct *coverages = GET_THREAD()->cfp->iseq->coverage; + + if (coverages && coverages->decisions) { + VALUE coverage = coverages->decisions; + VALUE line = LONG2FIX(rb_sourceline()); + VALUE counts = rb_hash_lookup(coverage, line); /* [true counts, false counts] */ + VALUE count; + + if (counts == Qnil) { + return; + } + + if (event == RUBY_EVENT_DCOVERAGE_TRUE) { + count = FIX2LONG(RARRAY_AREF(counts, 0)) + 1; + RARRAY_ASET(counts, 0, LONG2FIX(count)); + } + else { + count = FIX2LONG(RARRAY_AREF(counts, 1)) + 1; + RARRAY_ASET(counts, 1, LONG2FIX(count)); + } + } } VALUE @@ -5169,8 +5189,9 @@ struct exec_recursive_params { { GET_VM()->coverages = coverages; rb_add_event_hook(update_coverage, RUBY_EVENT_COVERAGE, Qnil); - rb_add_event_hook(update_detailed_coverage, RUBY_EVENT_MCOVERAGE, Qnil); - rb_add_event_hook(update_detailed_coverage, RUBY_EVENT_BCOVERAGE, Qnil); + rb_add_event_hook(update_method_coverage, RUBY_EVENT_MCOVERAGE, Qnil); + rb_add_event_hook(update_decision_coverage, RUBY_EVENT_DCOVERAGE_TRUE, Qnil); + rb_add_event_hook(update_decision_coverage, RUBY_EVENT_DCOVERAGE_FALSE, Qnil); } void @@ -5178,7 +5199,8 @@ struct exec_recursive_params { { GET_VM()->coverages = Qfalse; rb_remove_event_hook(update_coverage); - rb_remove_event_hook(update_detailed_coverage); + rb_remove_event_hook(update_method_coverage); + rb_remove_event_hook(update_decision_coverage); } VALUE diff --git a/vm_core.h b/vm_core.h index 91647e5..ad098c2 100644 --- a/vm_core.h +++ b/vm_core.h @@ -183,7 +183,7 @@ struct iseq_inline_cache_entry { struct rb_coverage_struct { const VALUE lines; const VALUE methods; - const VALUE branches; + const VALUE decisions; }; struct rb_iseq_struct; -- 1.8.5.5