Revamp method coverage to support define_method

Traditionally, method coverage measurement was implemented by inserting
`trace2` instruction to the head of method iseq.  So, it just measured
methods defined by `def` keyword.

This commit drastically changes the measuring mechanism of method
coverage; at `RUBY_EVENT_CALL`, it keeps a hash from rb_method_entry_t*
to runs (i.e., it counts the runs per method entry), and at
`Coverage.result`, it creates the result hash by enumerating all
`rb_method_entry_t*` objects (by `ObjectSpace.each_object`).

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@61023 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
mame 2017-12-05 07:16:42 +00:00
Родитель aa87ae7a04
Коммит 0a6816ecd7
11 изменённых файлов: 285 добавлений и 81 удалений

Просмотреть файл

@ -294,19 +294,6 @@ struct iseq_compile_data_ensure_node_stack {
ADD_INSN2((seq), (first_line), trace2, INT2FIX(RUBY_EVENT_COVERAGE), INT2FIX(counter_idx * 16 + COVERAGE_INDEX_BRANCHES)); \
} \
} while (0)
#define ADD_TRACE_METHOD_COVERAGE(seq, line, method_name) \
do { \
if (ISEQ_COVERAGE(iseq) && \
ISEQ_METHOD_COVERAGE(iseq) && \
(line) > 0) { \
VALUE methods = ISEQ_METHOD_COVERAGE(iseq); \
long counter_idx = RARRAY_LEN(methods) / 3; \
rb_ary_push(methods, ID2SYM(method_name)); \
rb_ary_push(methods, INT2FIX(line)); \
rb_ary_push(methods, INT2FIX(0)); \
ADD_INSN2((seq), (line), trace2, INT2FIX(RUBY_EVENT_COVERAGE), INT2FIX(counter_idx * 16 + COVERAGE_INDEX_METHODS)); \
} \
} while (0)
static void iseq_add_getlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, int line, int idx, int level);
static void iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, int line, int idx, int level);
@ -670,7 +657,6 @@ rb_iseq_compile_node(rb_iseq_t *iseq, const NODE *node)
case ISEQ_TYPE_METHOD:
{
ADD_TRACE(ret, RUBY_EVENT_CALL);
ADD_TRACE_METHOD_COVERAGE(ret, FIX2INT(iseq->body->location.first_lineno), rb_intern_str(iseq->body->location.label));
CHECK(COMPILE(ret, "scoped node", node->nd_body));
ADD_TRACE(ret, RUBY_EVENT_RETURN);
break;

Просмотреть файл

@ -10,8 +10,10 @@
#include "ruby.h"
#include "vm_core.h"
#include "gc.h"
static int current_mode;
static VALUE me2counter = Qnil;
/*
* call-seq:
@ -55,13 +57,20 @@ rb_coverage_start(int argc, VALUE *argv, VALUE klass)
}
}
if (mode & COVERAGE_TARGET_METHODS) {
me2counter = rb_hash_new_compare_by_id();
}
else {
me2counter = Qnil;
}
coverages = rb_get_coverages();
if (!RTEST(coverages)) {
coverages = rb_hash_new();
rb_obj_hide(coverages);
current_mode = mode;
if (mode == 0) mode = COVERAGE_TARGET_LINES;
rb_set_coverages(coverages, mode);
rb_set_coverages(coverages, mode, me2counter);
}
else if (current_mode != mode) {
rb_raise(rb_eRuntimeError, "cannot change the measuring target during coverage measurement");
@ -101,21 +110,60 @@ branch_coverage(VALUE branches)
return ret;
}
static VALUE
method_coverage(VALUE methods)
static int
method_coverage_i(void *vstart, void *vend, size_t stride, void *data)
{
VALUE ret = rb_hash_new();
int i;
long id = 0;
/*
* ObjectSpace.each_object(Module){|mod|
* mod.instance_methods.each{|mid|
* m = mod.instance_method(mid)
* if loc = m.source_location
* p [m.name, loc, $g_method_cov_counts[m]]
* end
* }
* }
*/
VALUE ncoverages = *(VALUE*)data, v;
for (i = 0; i < RARRAY_LEN(methods); ) {
VALUE method_name = RARRAY_AREF(methods, i++);
VALUE lineno = RARRAY_AREF(methods, i++);
VALUE counter = RARRAY_AREF(methods, i++);
rb_hash_aset(ret, rb_ary_new_from_args(3, method_name, LONG2FIX(id++), lineno), counter);
for (v = (VALUE)vstart; v != (VALUE)vend; v += stride) {
if (RB_TYPE_P(v, T_IMEMO) && imemo_type(v) == imemo_ment) {
const rb_method_entry_t *me = (rb_method_entry_t *) v;
VALUE path = Qundef, first_lineno = Qundef;
VALUE data[2], ncoverage, methods;
VALUE methods_id = ID2SYM(rb_intern("methods"));
VALUE klass;
const rb_method_entry_t *me2 = rb_resolve_me_location(me, data);
if (me != me2) continue;
klass = me->owner;
if (RB_TYPE_P(klass, T_ICLASS)) {
rb_bug("T_ICLASS");
}
path = data[0];
first_lineno = data[1];
if (FIX2LONG(first_lineno) <= 0) continue;
ncoverage = rb_hash_aref(ncoverages, path);
if (NIL_P(ncoverage)) continue;
methods = rb_hash_aref(ncoverage, methods_id);
{
VALUE method_id = ID2SYM(me->def->original_id);
VALUE rcount = rb_hash_aref(me2counter, (VALUE) me);
VALUE key = rb_ary_new_from_args(3, klass, method_id, first_lineno);
VALUE rcount2 = rb_hash_aref(methods, key);
if (NIL_P(rcount)) rcount = LONG2FIX(0);
if (NIL_P(rcount2)) rcount2 = LONG2FIX(0);
if (!POSFIXABLE(FIX2LONG(rcount) + FIX2LONG(rcount2))) {
rcount = LONG2FIX(FIXNUM_MAX);
}
else {
rcount = LONG2FIX(FIX2LONG(rcount) + FIX2LONG(rcount2));
}
rb_hash_aset(methods, key, rcount);
}
}
}
return ret;
return 0;
}
static int
@ -132,25 +180,23 @@ coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h)
}
else {
VALUE h = rb_hash_new();
VALUE lines = RARRAY_AREF(coverage, COVERAGE_INDEX_LINES);
VALUE branches = RARRAY_AREF(coverage, COVERAGE_INDEX_BRANCHES);
VALUE methods = RARRAY_AREF(coverage, COVERAGE_INDEX_METHODS);
if (lines) {
if (current_mode & COVERAGE_TARGET_LINES) {
VALUE lines = RARRAY_AREF(coverage, COVERAGE_INDEX_LINES);
lines = rb_ary_dup(lines);
rb_ary_freeze(lines);
rb_hash_aset(h, ID2SYM(rb_intern("lines")), lines);
}
if (branches) {
if (current_mode & COVERAGE_TARGET_BRANCHES) {
VALUE branches = RARRAY_AREF(coverage, COVERAGE_INDEX_BRANCHES);
rb_hash_aset(h, ID2SYM(rb_intern("branches")), branch_coverage(branches));
}
if (methods) {
rb_hash_aset(h, ID2SYM(rb_intern("methods")), method_coverage(methods));
if (current_mode & COVERAGE_TARGET_METHODS) {
rb_hash_aset(h, ID2SYM(rb_intern("methods")), rb_hash_new());
}
rb_hash_freeze(h);
coverage = h;
}
@ -178,6 +224,11 @@ rb_coverage_peek_result(VALUE klass)
rb_raise(rb_eRuntimeError, "coverage measurement is not enabled");
}
st_foreach(RHASH_TBL(coverages), coverage_peek_result_i, ncoverages);
if (current_mode & COVERAGE_TARGET_METHODS) {
rb_objspace_each_objects(method_coverage_i, &ncoverages);
}
rb_hash_freeze(ncoverages);
return ncoverages;
}
@ -194,6 +245,7 @@ rb_coverage_result(VALUE klass)
{
VALUE ncoverages = rb_coverage_peek_result(klass);
rb_reset_coverages();
me2counter = Qnil;
return ncoverages;
}
@ -252,4 +304,5 @@ Init_coverage(void)
rb_define_module_function(rb_mCoverage, "result", rb_coverage_result, 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);
}

8
hash.c
Просмотреть файл

@ -426,6 +426,14 @@ rb_hash_new(void)
return hash_alloc(rb_cHash);
}
VALUE
rb_hash_new_compare_by_id(void)
{
VALUE hash = rb_hash_new();
RHASH(hash)->ntbl = rb_init_identtable();
return hash;
}
VALUE
rb_hash_new_with_size(st_index_t size)
{

Просмотреть файл

@ -1272,6 +1272,9 @@ void ruby_sized_xfree(void *x, size_t size);
/* hash.c */
struct st_table *rb_hash_tbl_raw(VALUE hash);
VALUE rb_hash_new_with_size(st_index_t size);
RUBY_SYMBOL_EXPORT_BEGIN
VALUE rb_hash_new_compare_by_id(void);
RUBY_SYMBOL_EXPORT_END
VALUE rb_hash_has_key(VALUE hash, VALUE key);
VALUE rb_hash_default_value(VALUE hash, VALUE key);
VALUE rb_hash_set_default_proc(VALUE hash, VALUE proc);
@ -1758,7 +1761,6 @@ struct timeval rb_time_timeval(VALUE);
/* thread.c */
#define COVERAGE_INDEX_LINES 0
#define COVERAGE_INDEX_BRANCHES 1
#define COVERAGE_INDEX_METHODS 2
#define COVERAGE_TARGET_LINES 1
#define COVERAGE_TARGET_BRANCHES 2
#define COVERAGE_TARGET_METHODS 4

1
iseq.h
Просмотреть файл

@ -51,7 +51,6 @@ iseq_mark_ary_create(int flip_cnt)
#define ISEQ_COVERAGE_SET(iseq, cov) RARRAY_ASET(ISEQ_MARK_ARY(iseq), ISEQ_MARK_ARY_COVERAGE, cov)
#define ISEQ_LINE_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_LINES)
#define ISEQ_BRANCH_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_BRANCHES)
#define ISEQ_METHOD_COVERAGE(iseq) RARRAY_AREF(ISEQ_COVERAGE(iseq), COVERAGE_INDEX_METHODS)
#define ISEQ_FLIP_CNT(iseq) FIX2INT(RARRAY_AREF(ISEQ_MARK_ARY(iseq), ISEQ_MARK_ARY_FLIP_CNT))

Просмотреть файл

@ -190,6 +190,9 @@ const rb_method_entry_t *rb_method_entry_at(VALUE obj, ID id);
const rb_method_entry_t *rb_method_entry(VALUE klass, ID id);
const rb_method_entry_t *rb_method_entry_without_refinements(VALUE klass, ID id, VALUE *defined_class);
const rb_method_entry_t *rb_resolve_refined_method(VALUE refinements, const rb_method_entry_t *me);
RUBY_SYMBOL_EXPORT_BEGIN
const rb_method_entry_t *rb_resolve_me_location(const rb_method_entry_t *, VALUE[2]);
RUBY_SYMBOL_EXPORT_END
const rb_callable_method_entry_t *rb_callable_method_entry(VALUE klass, ID id);
const rb_callable_method_entry_t *rb_callable_method_entry_with_refinements(VALUE klass, ID id, VALUE *defined_class);

Просмотреть файл

@ -188,7 +188,15 @@ class TestCoverage < Test::Unit::TestCase
Coverage.start(#{ opt })
tmp = Dir.pwd
require tmp + '/test.rb'
p Coverage.result[tmp + "/test.rb"]
r = Coverage.result[tmp + "/test.rb"]
if r[:methods]
h = {}
r[:methods].keys.sort_by {|key| key.drop(1) }.each do |key|
h[key] = r[:methods][key]
end
r[:methods].replace h
end
p r
end;
}
}
@ -332,9 +340,9 @@ class TestCoverage < Test::Unit::TestCase
def test_method_coverage
result = {
:methods => {
[:foo, 0, 1] => 2,
[:bar, 1, 2] => 1,
[:baz, 2, 4] => 0,
[Object, :bar, 2] => 1,
[Object, :baz, 4] => 0,
[Object, :foo, 1] => 2,
}
}
assert_coverage(<<-"end;", { methods: true }, result)
@ -348,4 +356,101 @@ class TestCoverage < Test::Unit::TestCase
bar
end;
end
def test_method_coverage_for_define_method
result = {
:methods => {
[Object, :bar, 2] => 1,
[Object, :baz, 4] => 0,
[Object, :foo, 1] => 2,
}
}
assert_coverage(<<-"end;", { methods: true }, result)
define_method(:foo) {}
define_method(:bar) {
}
f = proc {}
define_method(:baz, &f)
foo
foo
bar
end;
end
class DummyConstant < String
def inspect
self
end
end
def test_method_coverage_for_alias
_C = DummyConstant.new("C")
_M = DummyConstant.new("M")
code = <<-"end;"
module M
def foo
end
alias bar foo
end
class C
include M
def baz
end
alias qux baz
end
end;
result = {
:methods => {
[_C, :baz, 8] => 0,
[_M, :foo, 2] => 0,
}
}
assert_coverage(code, { methods: true }, result)
result = {
:methods => {
[_C, :baz, 8] => 12,
[_M, :foo, 2] => 3,
}
}
assert_coverage(code + <<-"end;", { methods: true }, result)
obj = C.new
1.times { obj.foo }
2.times { obj.bar }
4.times { obj.baz }
8.times { obj.qux }
end;
end
def test_method_coverage_for_singleton_class
_singleton_Foo = DummyConstant.new("#<Class:Foo>")
_Foo = DummyConstant.new("Foo")
code = <<-"end;"
class Foo
def foo
end
alias bar foo
def self.baz
end
class << self
alias qux baz
end
end
1.times { Foo.new.foo }
2.times { Foo.new.bar }
4.times { Foo.baz }
8.times { Foo.qux }
end;
result = {
:methods => {
[_singleton_Foo, :baz, 5] => 12,
[_Foo, :foo, 2] => 3,
}
}
assert_coverage(code, { methods: true }, result)
end
end

109
thread.c
Просмотреть файл

@ -71,6 +71,7 @@
#include "ruby/thread_native.h"
#include "ruby/debug.h"
#include "internal.h"
#include "iseq.h"
#ifndef USE_NATIVE_THREAD_PRIORITY
#define USE_NATIVE_THREAD_PRIORITY 0
@ -4093,7 +4094,6 @@ clear_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);
VALUE methods = RARRAY_AREF(coverage, COVERAGE_INDEX_METHODS);
if (lines) {
for (i = 0; i < RARRAY_LEN(lines); i++) {
@ -4108,11 +4108,6 @@ clear_coverage_i(st_data_t key, st_data_t val, st_data_t dummy)
RARRAY_ASET(counters, i, INT2FIX(0));
}
}
if (methods) {
for (i = 2; i < RARRAY_LEN(methods); i += 3) {
RARRAY_ASET(methods, i, INT2FIX(0));
}
}
return ST_CONTINUE;
}
@ -5019,23 +5014,75 @@ update_coverage(VALUE data, const rb_trace_arg_t *trace_arg)
}
break;
}
case COVERAGE_INDEX_METHODS: {
VALUE methods = RARRAY_AREF(coverage, COVERAGE_INDEX_METHODS);
if (methods) {
long count;
long idx = arg / 16 * 3 + 2;
VALUE num = RARRAY_AREF(methods, idx);
count = FIX2LONG(num) + 1;
if (POSFIXABLE(count)) {
RARRAY_ASET(methods, idx, LONG2FIX(count));
}
}
break;
}
}
}
}
const rb_method_entry_t *
rb_resolve_me_location(const rb_method_entry_t *me, VALUE resolved_location[2])
{
VALUE path, first_lineno;
retry:
switch (me->def->type) {
case VM_METHOD_TYPE_ISEQ: {
rb_iseq_location_t loc = me->def->body.iseq.iseqptr->body->location;
path = loc.pathobj;
first_lineno = loc.first_lineno;
break;
}
case VM_METHOD_TYPE_BMETHOD: {
const rb_iseq_t *iseq = rb_proc_get_iseq(me->def->body.proc, 0);
if (iseq) {
rb_iseq_check(iseq);
path = rb_iseq_path(iseq);
first_lineno = iseq->body->location.first_lineno;
break;
}
return NULL;
}
case VM_METHOD_TYPE_ALIAS:
me = me->def->body.alias.original_me;
goto retry;
case VM_METHOD_TYPE_REFINED:
me = me->def->body.refined.orig_me;
if (!me) return NULL;
goto retry;
default:
return NULL;
}
/* found */
if (RB_TYPE_P(path, T_ARRAY)) {
path = rb_ary_entry(path, 1);
if (!RB_TYPE_P(path, T_STRING)) return NULL; /* just for the case... */
}
if (resolved_location) {
resolved_location[0] = path;
resolved_location[1] = first_lineno;
}
return me;
}
static void
update_method_coverage(VALUE me2counter, rb_trace_arg_t *trace_arg)
{
const rb_control_frame_t *cfp = GET_EC()->cfp;
const rb_callable_method_entry_t *cme = rb_vm_frame_method_entry(cfp);
const rb_method_entry_t *me = (const rb_method_entry_t *)cme;
VALUE rcount;
long count;
me = rb_resolve_me_location(me, 0);
if (!me) return;
rcount = rb_hash_aref(me2counter, (VALUE) me);
count = FIXNUM_P(rcount) ? FIX2LONG(rcount) + 1 : 1;
if (POSFIXABLE(count)) {
rb_hash_aset(me2counter, (VALUE) me, LONG2FIX(count));
}
}
VALUE
rb_get_coverages(void)
{
@ -5043,11 +5090,14 @@ rb_get_coverages(void)
}
void
rb_set_coverages(VALUE coverages, int mode)
rb_set_coverages(VALUE coverages, int mode, VALUE me2counter)
{
GET_VM()->coverages = coverages;
GET_VM()->coverage_mode = mode;
rb_add_event_hook2((rb_event_hook_func_t) update_coverage, RUBY_EVENT_COVERAGE, Qnil, RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG);
if (mode & COVERAGE_TARGET_METHODS) {
rb_add_event_hook2((rb_event_hook_func_t) update_method_coverage, RUBY_EVENT_CALL, me2counter, RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG);
}
}
/* Make coverage arrays empty so old covered files are no longer tracked. */
@ -5057,10 +5107,8 @@ 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);
VALUE methods = RARRAY_AREF(coverage, COVERAGE_INDEX_METHODS);
if (lines) rb_ary_clear(lines);
if (branches) rb_ary_clear(branches);
if (methods) rb_ary_clear(methods);
return ST_CONTINUE;
}
@ -5071,13 +5119,16 @@ rb_reset_coverages(void)
st_foreach(rb_hash_tbl_raw(coverages), reset_coverage_i, 0);
GET_VM()->coverages = Qfalse;
rb_remove_event_hook((rb_event_hook_func_t) update_coverage);
if (GET_VM()->coverage_mode & COVERAGE_TARGET_METHODS) {
rb_remove_event_hook((rb_event_hook_func_t) update_method_coverage);
}
}
VALUE
rb_default_coverage(int n)
{
VALUE coverage = rb_ary_tmp_new_fill(3);
VALUE lines = Qfalse, branches = Qfalse, methods = Qfalse;
VALUE lines = Qfalse, branches = Qfalse;
int mode = GET_VM()->coverage_mode;
if (mode & COVERAGE_TARGET_LINES) {
@ -5105,18 +5156,6 @@ rb_default_coverage(int n)
}
RARRAY_ASET(coverage, COVERAGE_INDEX_BRANCHES, branches);
if (mode & COVERAGE_TARGET_METHODS) {
methods = rb_ary_tmp_new(0);
/* internal data structures for method coverage:
*
* [symbol_of_method_name, lineno_of_method_head, counter,
* ...]
*
* Example: [:foobar, 1, 0, ...]
*/
}
RARRAY_ASET(coverage, COVERAGE_INDEX_METHODS, methods);
return coverage;
}

Просмотреть файл

@ -87,15 +87,15 @@ def gen_rb_lcov(file)
# function coverage
total = covered = 0
cov[:methods].each do |(name, _, lineno), count|
f.puts "FN:#{ lineno },#{ name }"
cov[:methods].each do |(klass, name, lineno), count|
f.puts "FN:#{ lineno },#{ klass }##{ name }"
total += 1
covered += 1 if count > 0
end
f.puts "FNF:#{ total }"
f.puts "FNF:#{ covered }"
cov[:methods].each do |(name, _), count|
f.puts "FNDA:#{ count },#{ name }"
cov[:methods].each do |(klass, name, _), count|
f.puts "FNDA:#{ count },#{ klass }##{ name }"
end
# line coverage

Просмотреть файл

@ -41,6 +41,15 @@ def add_count(h, key, count)
end
def save_coverage_data(res1)
res1.each do |_path, cov|
if cov[:methods]
h = {}
cov[:methods].each do |(klass, *key), count|
h[[klass.inspect, *key]] = count
end
cov[:methods].replace h
end
end
File.open(TEST_COVERAGE_DATA_FILE, File::RDWR | File::CREAT | File::BINARY) do |f|
f.flock(File::LOCK_EX)
s = f.read

Просмотреть файл

@ -1753,7 +1753,7 @@ RUBY_SYMBOL_EXPORT_BEGIN
int rb_thread_check_trap_pending(void);
extern VALUE rb_get_coverages(void);
extern void rb_set_coverages(VALUE, int);
extern void rb_set_coverages(VALUE, int, VALUE);
extern void rb_reset_coverages(void);
void rb_postponed_job_flush(rb_vm_t *vm);