Feature #15022 ยป oneshot-coverage.patch
| compile.c | ||
|---|---|---|
| 		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)); | ||
| 		} | ||
| ext/coverage/coverage.c | ||
|---|---|---|
| 	    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) { | ||
| ... | ... | |
| 	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) { | ||
| ... | ... | |
| static VALUE | ||
| rb_coverage_result(VALUE klass) | ||
| { | ||
|     VALUE ncoverages = rb_coverage_peek_result(klass); | ||
|     VALUE ncoverages; | ||
|     ncoverages = rb_coverage_peek_result(klass); | ||
|     rb_reset_coverages(); | ||
|     me2counter = Qnil; | ||
|     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.clear  => nil | ||
|  * | ||
|  * Clears the internal counters of coverage. | ||
|  */ | ||
| static VALUE | ||
| rb_coverage_clear(VALUE klass) | ||
| { | ||
|     rb_clear_coverages(); | ||
|     if (!NIL_P(me2counter)) rb_hash_foreach(me2counter, clear_me2counter_i, Qnil); | ||
|     return Qnil; | ||
| } | ||
| /* | ||
|  *  call-seq: | ||
|  *     Coverage.running?  => bool | ||
| ... | ... | |
|     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, "clear", rb_coverage_clear, 0); | ||
|     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); | ||
| internal.h | ||
|---|---|---|
| #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); | ||
| iseq.c | ||
|---|---|---|
|     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); | ||
|         } | ||
|     } | ||
| ... | ... | |
|     } | ||
| } | ||
| 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) | ||
| { | ||
| ... | ... | |
|     rb_bug("trace_instrument: invalid insn address: %p", (void *)*iseq_encoded_insn); | ||
| } | ||
| 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) | ||
| { | ||
| test/coverage/test_coverage.rb | ||
|---|---|---|
|     } | ||
|     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.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_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.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[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.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[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.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[f] | ||
|           Coverage.clear | ||
|           foo(-1) | ||
|           p Coverage.peek_result[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 | ||
| thread.c | ||
|---|---|---|
|     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); | ||
| ... | ... | |
|     return ST_CONTINUE; | ||
| } | ||
| static void | ||
| clear_coverage(void) | ||
| void | ||
| rb_clear_coverages(void) | ||
| { | ||
|     VALUE coverages = rb_get_coverages(); | ||
|     if (RTEST(coverages)) { | ||
| ... | ... | |
|     vm->fork_gen++; | ||
|     vm->sleeper = 0; | ||
|     clear_coverage(); | ||
|     rb_clear_coverages(); | ||
| } | ||
| static void | ||
| ... | ... | |
| 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; | ||
| 	    } | ||
| ... | ... | |
|     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) | ||
| { | ||
| ... | ... | |
| } | ||
| /* 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); | ||
| vm_core.h | ||
|---|---|---|
| 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); | ||