ruby-changes:48905
From: mame <ko1@a...>
Date: Tue, 5 Dec 2017 16:16:48 +0900 (JST)
Subject: [ruby-changes:48905] mame:r61023 (trunk): Revamp method coverage to support define_method
mame 2017-12-05 16:16:42 +0900 (Tue, 05 Dec 2017) New Revision: 61023 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=61023 Log: 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`). Modified files: trunk/compile.c trunk/ext/coverage/coverage.c trunk/hash.c trunk/internal.h trunk/iseq.h trunk/method.h trunk/test/coverage/test_coverage.rb trunk/thread.c trunk/tool/run-lcov.rb trunk/tool/test-coverage.rb trunk/vm_core.h Index: method.h =================================================================== --- method.h (revision 61022) +++ method.h (revision 61023) @@ -190,6 +190,9 @@ const rb_method_entry_t *rb_method_entry https://github.com/ruby/ruby/blob/trunk/method.h#L190 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); Index: vm_core.h =================================================================== --- vm_core.h (revision 61022) +++ vm_core.h (revision 61023) @@ -1753,7 +1753,7 @@ RUBY_SYMBOL_EXPORT_BEGIN https://github.com/ruby/ruby/blob/trunk/vm_core.h#L1753 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); Index: iseq.h =================================================================== --- iseq.h (revision 61022) +++ iseq.h (revision 61023) @@ -51,7 +51,6 @@ iseq_mark_ary_create(int flip_cnt) https://github.com/ruby/ruby/blob/trunk/iseq.h#L51 #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)) Index: compile.c =================================================================== --- compile.c (revision 61022) +++ compile.c (revision 61023) @@ -294,19 +294,6 @@ struct iseq_compile_data_ensure_node_sta https://github.com/ruby/ruby/blob/trunk/compile.c#L294 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, co https://github.com/ruby/ruby/blob/trunk/compile.c#L657 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; Index: thread.c =================================================================== --- thread.c (revision 61022) +++ thread.c (revision 61023) @@ -71,6 +71,7 @@ https://github.com/ruby/ruby/blob/trunk/thread.c#L71 #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_ https://github.com/ruby/ruby/blob/trunk/thread.c#L4094 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_ https://github.com/ruby/ruby/blob/trunk/thread.c#L4108 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,20 +5014,72 @@ update_coverage(VALUE data, const rb_tra https://github.com/ruby/ruby/blob/trunk/thread.c#L5014 } 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)); - } - } + } + } +} + +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)); } } @@ -5043,11 +5090,14 @@ rb_get_coverages(void) https://github.com/ruby/ruby/blob/trunk/thread.c#L5090 } 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_ https://github.com/ruby/ruby/blob/trunk/thread.c#L5107 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) https://github.com/ruby/ruby/blob/trunk/thread.c#L5119 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) https://github.com/ruby/ruby/blob/trunk/thread.c#L5156 } 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; } Index: ext/coverage/coverage.c =================================================================== --- ext/coverage/coverage.c (revision 61022) +++ ext/coverage/coverage.c (revision 61023) @@ -10,8 +10,10 @@ https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L10 #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, https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L57 } } + 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) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L110 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; - - 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); + /* + * 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 (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 https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L180 } 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) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L224 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) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L245 { VALUE ncoverages = rb_coverage_peek_result(klass); rb_reset_coverages(); + me2counter = Qnil; return ncoverages; } @@ -252,4 +304,5 @@ Init_coverage(void) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L304 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); } Index: hash.c =================================================================== --- hash.c (revision 61022) +++ hash.c (revision 61023) @@ -427,6 +427,14 @@ rb_hash_new(void) https://github.com/ruby/ruby/blob/trunk/hash.c#L427 } 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) { VALUE ret = rb_hash_new(); Index: internal.h =================================================================== --- internal.h (revision 61022) +++ internal.h (revision 61023) @@ -1272,6 +1272,9 @@ void ruby_sized_xfree(void *x, size_t si https://github.com/ruby/ruby/blob/trunk/internal.h#L1272 /* 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); https://github.com/ruby/ruby/blob/trunk/internal.h#L1761 /* 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 Index: tool/run-lcov.rb =================================================================== --- tool/run-lcov.rb (revision 61022) +++ tool/run-lcov.rb (revision 61023) @@ -87,15 +87,15 @@ def gen_rb_lcov(file) https://github.com/ruby/ruby/blob/trunk/tool/run-lcov.rb#L87 # 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 Index: tool/test-coverage.rb =================================================================== --- tool/test-coverage.rb (revision 61022) +++ tool/test-coverage.rb (revision 61023) @@ -41,6 +41,15 @@ def add_count(h, key, count) https://github.com/ruby/ruby/blob/trunk/tool/test-coverage.rb#L41 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 Index: test/coverage/test_coverage.rb =================================================================== --- test/coverage/test_coverage.rb (revision 61022) +++ test/coverage/test_coverage.rb (revision 61023) @@ -188,7 +188,15 @@ class TestCoverage < Test::Unit::TestCas https://github.com/ruby/ruby/blob/trunk/test/coverage/test_coverage.rb#L188 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::TestCas https://github.com/ruby/ruby/blob/trunk/test/coverage/test_coverage.rb#L340 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::TestCas https://github.com/ruby/ruby/blob/trunk/test/coverage/test_coverage.rb#L356 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) (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/