[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]