ruby-changes:52981
From: mame <ko1@a...>
Date: Sat, 20 Oct 2018 14:33:09 +0900 (JST)
Subject: [ruby-changes:52981] mame:r65195 (trunk): ext/coverage/: add the oneshot mode
mame 2018-10-20 14:33:04 +0900 (Sat, 20 Oct 2018) New Revision: 65195 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=65195 Log: ext/coverage/: add the oneshot mode This patch introduces "oneshot_lines" mode for `Coverage.start`, which checks "whether each line was executed at least once or not", instead of "how many times each line was executed". A hook for each line is fired at most once, and after it is fired, the hook flag was removed; it runs with zero overhead. See [Feature #15022] in detail. Added directories: trunk/ext/coverage/lib/ Added files: trunk/ext/coverage/lib/coverage.rb Modified files: trunk/compile.c trunk/ext/coverage/coverage.c trunk/internal.h trunk/iseq.c trunk/test/coverage/test_coverage.rb trunk/thread.c trunk/vm_core.h Index: ext/coverage/lib/coverage.rb =================================================================== --- ext/coverage/lib/coverage.rb (nonexistent) +++ ext/coverage/lib/coverage.rb (revision 65195) @@ -0,0 +1,14 @@ https://github.com/ruby/ruby/blob/trunk/ext/coverage/lib/coverage.rb#L1 +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 Index: ext/coverage/coverage.c =================================================================== --- ext/coverage/coverage.c (revision 65194) +++ ext/coverage/coverage.c (revision 65195) @@ -45,9 +45,12 @@ rb_coverage_start(int argc, VALUE *argv, https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L45 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 https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L182 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 https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L209 * 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) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L234 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) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L334 { 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); Index: iseq.c =================================================================== --- iseq.c (revision 65194) +++ iseq.c (revision 65195) @@ -656,7 +656,8 @@ rb_iseq_new_top(const rb_ast_body_t *ast https://github.com/ruby/ruby/blob/trunk/iseq.c#L656 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 *ise https://github.com/ruby/ruby/blob/trunk/iseq.c#L1656 } } +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 *ise https://github.com/ruby/ruby/blob/trunk/iseq.c#L2949 } 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); Index: thread.c =================================================================== --- thread.c (revision 65194) +++ thread.c (revision 65195) @@ -4323,11 +4323,16 @@ clear_coverage_i(st_data_t key, st_data_ https://github.com/ruby/ruby/blob/trunk/thread.c#L4323 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_ https://github.com/ruby/ruby/blob/trunk/thread.c#L4344 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 *t https://github.com/ruby/ruby/blob/trunk/thread.c#L4378 vm->fork_gen++; vm->sleeper = 0; - clear_coverage(); + rb_clear_coverages(); } static void @@ -5219,13 +5224,20 @@ rb_check_deadlock(rb_vm_t *vm) https://github.com/ruby/ruby/blob/trunk/thread.c#L5224 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) https://github.com/ruby/ruby/blob/trunk/thread.c#L5352 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 mo https://github.com/ruby/ruby/blob/trunk/thread.c#L5373 } /* 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); Index: internal.h =================================================================== --- internal.h (revision 65194) +++ internal.h (revision 65195) @@ -1852,12 +1852,14 @@ struct timeval rb_time_timeval(VALUE); https://github.com/ruby/ruby/blob/trunk/internal.h#L1852 #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); Index: vm_core.h =================================================================== --- vm_core.h (revision 65194) +++ vm_core.h (revision 65195) @@ -1837,6 +1837,7 @@ int rb_thread_check_trap_pending(void); https://github.com/ruby/ruby/blob/trunk/vm_core.h#L1837 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); Index: compile.c =================================================================== --- compile.c (revision 65194) +++ compile.c (revision 65195) @@ -2013,7 +2013,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ https://github.com/ruby/ruby/blob/trunk/compile.c#L2013 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)); } Index: test/coverage/test_coverage.rb =================================================================== --- test/coverage/test_coverage.rb (revision 65194) +++ test/coverage/test_coverage.rb (revision 65195) @@ -483,4 +483,196 @@ class TestCoverage < Test::Unit::TestCas https://github.com/ruby/ruby/blob/trunk/test/coverage/test_coverage.rb#L483 } 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 -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/