/************************************************ coverage.c - $Author: $ Copyright (c) 2008 Yusuke Endoh ************************************************/ #include "internal/gc.h" #include "internal/hash.h" #include "internal/thread.h" #include "internal/sanitizers.h" #include "ruby.h" #include "vm_core.h" static enum { IDLE, SUSPENDED, RUNNING } current_state = IDLE; static int current_mode; static VALUE me2counter = Qnil; /* * call-seq: Coverage.supported?(mode) -> true or false * * Returns true if coverage measurement is supported for the given mode. * * The mode should be one of the following symbols: * +:lines+, +:oneshot_lines+, +:branches+, +:methods+, +:eval+. * * Example: * * Coverage.supported?(:lines) #=> true * Coverage.supported?(:all) #=> false */ static VALUE rb_coverage_supported(VALUE self, VALUE _mode) { ID mode = RB_SYM2ID(_mode); return RBOOL( mode == rb_intern("lines") || mode == rb_intern("oneshot_lines") || mode == rb_intern("branches") || mode == rb_intern("methods") || mode == rb_intern("eval") ); } /* * call-seq: * Coverage.setup -> nil * Coverage.setup(type) -> nil * Coverage.setup(lines: false, branches: false, methods: false, eval: false, oneshot_lines: false) -> nil * * Performs setup for coverage measurement, but does not start coverage measurement. * To start coverage measurement, use Coverage.resume. * * To perform both setup and start coverage measurement, Coverage.start can be used. * * With argument +type+ given and +type+ is symbol +:all+, enables all types of coverage * (lines, branches, methods, and eval). * * Keyword arguments or hash +type+ can be given with each of the following keys: * * - +lines+: Enables line coverage that records the number of times each line was executed. * If +lines+ is enabled, +oneshot_lines+ cannot be enabled. * See {Lines Coverage}[rdoc-ref:Coverage@Lines+Coverage]. * - +branches+: Enables branch coverage that records the number of times each * branch in each conditional was executed. See {Branches Coverage}[rdoc-ref:Coverage@Branch+Coverage]. * - +methods+: Enables method coverage that records the number of times each method was exectued. * See {Methods Coverage}[rdoc-ref:Coverage@Methods+Coverage]. * - +eval+: Enables coverage for evaluations (e.g. Kernel#eval, Module#class_eval). * See {Eval Coverage}[rdoc-ref:Coverage@Eval+Coverage]. */ static VALUE rb_coverage_setup(int argc, VALUE *argv, VALUE klass) { VALUE coverages, opt; int mode; if (current_state != IDLE) { rb_raise(rb_eRuntimeError, "coverage measurement is already setup"); } rb_scan_args(argc, argv, "01", &opt); if (argc == 0) { mode = 0; /* compatible mode */ } else if (opt == ID2SYM(rb_intern("all"))) { mode = COVERAGE_TARGET_LINES | COVERAGE_TARGET_BRANCHES | COVERAGE_TARGET_METHODS | COVERAGE_TARGET_EVAL; } else { mode = 0; opt = rb_convert_type(opt, T_HASH, "Hash", "to_hash"); if (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("lines"))))) mode |= COVERAGE_TARGET_LINES; if (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("branches"))))) mode |= COVERAGE_TARGET_BRANCHES; if (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("methods"))))) mode |= COVERAGE_TARGET_METHODS; 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 (RTEST(rb_hash_lookup(opt, ID2SYM(rb_intern("eval"))))) mode |= COVERAGE_TARGET_EVAL; } if (mode & COVERAGE_TARGET_METHODS) { me2counter = rb_ident_hash_new(); } 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, me2counter); current_state = SUSPENDED; } else if (current_mode != mode) { rb_raise(rb_eRuntimeError, "cannot change the measuring target during coverage measurement"); } return Qnil; } /* * call-seq: * Coverage.resume => nil * * Start/resume the coverage measurement. * * Caveat: Currently, only process-global coverage measurement is supported. * You cannot measure per-thread coverage. If your process has multiple thread, * using Coverage.resume/suspend to capture code coverage executed from only * a limited code block, may yield misleading results. */ VALUE rb_coverage_resume(VALUE klass) { if (current_state == IDLE) { rb_raise(rb_eRuntimeError, "coverage measurement is not set up yet"); } if (current_state == RUNNING) { rb_raise(rb_eRuntimeError, "coverage measurement is already running"); } rb_resume_coverages(); current_state = RUNNING; return Qnil; } /* * call-seq: * Coverage.start => nil * Coverage.start(:all) => nil * Coverage.start(lines: bool, branches: bool, methods: bool, eval: bool) => nil * Coverage.start(oneshot_lines: true) => nil * * Enables the coverage measurement. * See the documentation of Coverage class in detail. * This is equivalent to Coverage.setup and Coverage.resume. */ static VALUE rb_coverage_start(int argc, VALUE *argv, VALUE klass) { rb_coverage_setup(argc, argv, klass); rb_coverage_resume(klass); return Qnil; } struct branch_coverage_result_builder { int id; VALUE result; VALUE children; VALUE counters; }; static int branch_coverage_ii(VALUE _key, VALUE branch, VALUE v) { struct branch_coverage_result_builder *b = (struct branch_coverage_result_builder *) v; VALUE target_label = RARRAY_AREF(branch, 0); VALUE target_first_lineno = RARRAY_AREF(branch, 1); VALUE target_first_column = RARRAY_AREF(branch, 2); VALUE target_last_lineno = RARRAY_AREF(branch, 3); VALUE target_last_column = RARRAY_AREF(branch, 4); long counter_idx = FIX2LONG(RARRAY_AREF(branch, 5)); rb_hash_aset(b->children, rb_ary_new_from_args(6, target_label, LONG2FIX(b->id++), target_first_lineno, target_first_column, target_last_lineno, target_last_column), RARRAY_AREF(b->counters, counter_idx)); return ST_CONTINUE; } static int branch_coverage_i(VALUE _key, VALUE branch_base, VALUE v) { struct branch_coverage_result_builder *b = (struct branch_coverage_result_builder *) v; VALUE base_type = RARRAY_AREF(branch_base, 0); VALUE base_first_lineno = RARRAY_AREF(branch_base, 1); VALUE base_first_column = RARRAY_AREF(branch_base, 2); VALUE base_last_lineno = RARRAY_AREF(branch_base, 3); VALUE base_last_column = RARRAY_AREF(branch_base, 4); VALUE branches = RARRAY_AREF(branch_base, 5); VALUE children = rb_hash_new(); rb_hash_aset(b->result, rb_ary_new_from_args(6, base_type, LONG2FIX(b->id++), base_first_lineno, base_first_column, base_last_lineno, base_last_column), children); b->children = children; rb_hash_foreach(branches, branch_coverage_ii, v); return ST_CONTINUE; } static VALUE branch_coverage(VALUE branches) { VALUE structure = RARRAY_AREF(branches, 0); struct branch_coverage_result_builder b; b.id = 0; b.result = rb_hash_new(); b.counters = RARRAY_AREF(branches, 1); rb_hash_foreach(structure, branch_coverage_i, (VALUE)&b); return b.result; } static int method_coverage_i(void *vstart, void *vend, size_t stride, void *data) { /* * 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) { void *poisoned = rb_asan_poisoned_object_p(v); rb_asan_unpoison_object(v, false); 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, first_lineno, first_column, last_lineno, last_column; VALUE data[5], 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]; first_column = data[2]; last_lineno = data[3]; last_column = data[4]; 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(6, klass, method_id, first_lineno, first_column, last_lineno, last_column); 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); } } if (poisoned) { rb_asan_poison_object(v); } } return 0; } static int coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h) { VALUE path = (VALUE)key; VALUE coverage = (VALUE)val; VALUE coverages = (VALUE)h; if (current_mode == 0) { /* compatible mode */ VALUE lines = rb_ary_dup(RARRAY_AREF(coverage, COVERAGE_INDEX_LINES)); rb_ary_freeze(lines); coverage = lines; } else { VALUE h = rb_hash_new(); 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(kw)), lines); } 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 (current_mode & COVERAGE_TARGET_METHODS) { rb_hash_aset(h, ID2SYM(rb_intern("methods")), rb_hash_new()); } coverage = h; } rb_hash_aset(coverages, path, coverage); return ST_CONTINUE; } /* * call-seq: * 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], * ... * } */ static VALUE rb_coverage_peek_result(VALUE klass) { VALUE coverages = rb_get_coverages(); VALUE ncoverages = rb_hash_new(); if (!RTEST(coverages)) { rb_raise(rb_eRuntimeError, "coverage measurement is not enabled"); } rb_hash_foreach(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; } static int clear_me2counter_i(VALUE key, VALUE value, VALUE unused) { rb_hash_aset(me2counter, key, INT2FIX(0)); return ST_CONTINUE; } /* * call-seq: * Coverage.suspend => nil * * Suspend the coverage measurement. * You can use Coverage.resume to restart the measurement. */ VALUE rb_coverage_suspend(VALUE klass) { if (current_state != RUNNING) { rb_raise(rb_eRuntimeError, "coverage measurement is not running"); } rb_suspend_coverages(); current_state = SUSPENDED; return Qnil; } /* * call-seq: * Coverage.result(stop: true, clear: true) => hash * * 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(int argc, VALUE *argv, VALUE klass) { VALUE ncoverages; VALUE opt; int stop = 1, clear = 1; if (current_state == IDLE) { rb_raise(rb_eRuntimeError, "coverage measurement is not enabled"); } 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) { if (current_state == RUNNING) { rb_coverage_suspend(klass); } rb_reset_coverages(); me2counter = Qnil; current_state = IDLE; } return ncoverages; } /* * call-seq: * Coverage.state => :idle, :suspended, :running * * Returns the state of the coverage measurement. */ static VALUE rb_coverage_state(VALUE klass) { switch (current_state) { case IDLE: return ID2SYM(rb_intern("idle")); case SUSPENDED: return ID2SYM(rb_intern("suspended")); case RUNNING: return ID2SYM(rb_intern("running")); } return Qnil; } /* * call-seq: * Coverage.running? => bool * * Returns true if coverage stats are currently being collected (after * Coverage.start call, but before Coverage.result call) */ static VALUE rb_coverage_running(VALUE klass) { return current_state == RUNNING ? Qtrue : Qfalse; } /* \Coverage provides coverage measurement feature for Ruby. * * Only process-global coverage measurement is supported, meaning * that coverage cannot be measure on a per-thread basis. * * = Quick Start * * 1. Load coverage using require "coverage". * 2. Call Coverage.start to set up and begin coverage measurement. * 3. All Ruby code loaded following the call to Coverage.start will have * coverage measurement. * 4. Coverage results can be fetched by calling Coverage.result, which returns a * hash that contains filenames as the keys and coverage arrays as the values. * Each element of the coverage array gives the number of times each line was * executed. A +nil+ value means coverage was disabled for that line (e.g. * lines like +else+ and +end+). * * = Examples * * In file +fib.rb+: * * def fibonacci(n) * if n == 0 * 0 * elsif n == 1 * 1 * else * fibonacci(n - 1) + fibonacci(n - 2) * end * end * * puts fibonacci(10) * * In another file, coverage can be measured: * * require "coverage" * Coverage.start * require "fib.rb" * Coverage.result # => {"fib.rb" => [1, 177, 34, 143, 55, nil, 88, nil, nil, nil, 1]} * * == Lines \Coverage * * Lines coverage reports the number of line executions for each line. * If the coverage mode is not explicitly specified when starting coverage, * lines coverage is used as the default. * * require "coverage" * Coverage.start(lines: true) * require "fib" * Coverage.result # => {"fib.rb" => {lines: [1, 177, 34, 143, 55, nil, 88, nil, nil, nil, 1]}} * * The returned hash differs depending on how Coverage.setup or Coverage.start * was executed. * * If Coverage.start or Coverage.setup was called with no arguments, it returns a * hash which contains filenames as the keys and coverage arrays as the values. * * If Coverage.start or Coverage.setup was called with line: true, it * returns a hash which contains filenames as the keys and hashes as the values. * The value hash has a key +:lines+ where the value is a coverage array. * * Each element of the coverage array gives the number of times the line was * executed. A +nil+ value in the coverage array means coverage was disabled * for that line (e.g. lines like +else+ and +end+). * * == Oneshot Lines \Coverage * * Oneshot lines coverage is similar to lines coverage, but instead of reporting * the number of times a line was executed, it only reports the lines that were * executed. * * require "coverage" * Coverage.start(oneshot_lines: true) * require "fib" * Coverage.result # => {"fib.rb" => {oneshot_lines: [1, 11, 2, 4, 7, 5, 3]}} * * The value of the oneshot lines coverage result is an array containing the * line numbers that were executed. * * == Branches \Coverage * * Branches coverage reports the number of times each branch within each conditional * was executed. * * require "coverage" * Coverage.start(branches: true) * require "fib" * Coverage.result * # => {"fib.rb" => { * # branches: { * # [:if, 0, 2, 2, 8, 5] => { * # [:then, 1, 3, 4, 3, 5] => 34, * # [:else, 2, 4, 2, 8, 5] => 143}, * # [:if, 3, 4, 2, 8, 5] => { * # [:then, 4, 5, 4, 5, 5] => 55, * # [:else, 5, 7, 4, 7, 39] => 88}}}} * * Each entry within the branches hash is a conditional, the value of which is * another hash where each entry is a branch in that conditional. The keys are * arrays containing information about the branch and the values are the number * of times the branch was executed. * * The information that makes up the array that are the keys for conditional or * branches are the following, from left to right: * * 1. A label for the type of branch or conditional (e.g. +:if+, +:then+, +:else+). * 2. A unique identifier. * 3. Starting line number. * 4. Starting column number. * 5. Ending line number. * 6. Ending column number. * * == Methods \Coverage * * Methods coverage reports how many times each method was executed. * * require "coverage" * Coverage.start(methods: true) * require "fib" * p Coverage.result #=> {"fib.rb" => {methods: {[Object, :fibonacci, 1, 0, 9, 3] => 177}}} * * Each entry within the methods hash represents a method. The keys are arrays * containing hash are the number of times the method was executed, and the keys are * identifying information about the method. * * The information that makes up each key identifying a method is the following, * from left to right: * * 1. Class that the method was defined in. * 2. Method name as a Symbol. * 3. Starting line number of the method. * 4. Starting column number of the method. * 5. Ending line number of the method. * 6. Ending column number of the method. * * == Eval \Coverage * * Eval coverage can be combined with the coverage types above to track * coverage for eval. * * require "coverage" * Coverage.start(eval: true, lines: true) * * eval(<<~RUBY, nil, "eval 1") * ary = [] * 10.times do |i| * ary << "hello" * i * end * RUBY * * Coverage.result # => {"eval 1" => {lines: [1, 1, 10, nil]}} * * Note that the eval must have a filename assigned, otherwise coverage * will not be measured. * * require "coverage" * Coverage.start(eval: true, lines: true) * * eval(<<~RUBY) * ary = [] * 10.times do |i| * ary << "hello" * i * end * RUBY * * Coverage.result # => {"(eval)" => {lines: [nil, nil, nil, nil]}} * * Also note that if a line number is assigned to the eval and it is not 1, * then the resulting coverage will be padded with +nil+ if the line number is * greater than 1, and truncated if the line number is less than 1. * * require "coverage" * Coverage.start(eval: true, lines: true) * * eval(<<~RUBY, nil, "eval 1", 3) * ary = [] * 10.times do |i| * ary << "hello" * i * end * RUBY * * eval(<<~RUBY, nil, "eval 2", -1) * ary = [] * 10.times do |i| * ary << "hello" * i * end * RUBY * * Coverage.result * # => {"eval 1" => {lines: [nil, nil, 1, 1, 10, nil]}, "eval 2" => {lines: [10, nil]}} * * == All \Coverage Modes * * All modes of coverage can be enabled simultaneously using the Symbol +:all+. * However, note that this mode runs lines coverage and not oneshot lines since * they cannot be ran simultaneously. * * require "coverage" * Coverage.start(:all) * require "fib" * Coverage.result * # => {"fib.rb" => { * # lines: [1, 177, 34, 143, 55, nil, 88, nil, nil, nil, 1], * # branches: { * # [:if, 0, 2, 2, 8, 5] => { * # [:then, 1, 3, 4, 3, 5] => 34, * # [:else, 2, 4, 2, 8, 5] => 143}, * # [:if, 3, 4, 2, 8, 5] => { * # [:then, 4, 5, 4, 5, 5] => 55, * # [:else, 5, 7, 4, 7, 39] => 88}}}}, * # methods: {[Object, :fibonacci, 1, 0, 9, 3] => 177}}} */ void Init_coverage(void) { VALUE rb_mCoverage = rb_define_module("Coverage"); rb_define_singleton_method(rb_mCoverage, "supported?", rb_coverage_supported, 1); rb_define_module_function(rb_mCoverage, "setup", rb_coverage_setup, -1); rb_define_module_function(rb_mCoverage, "start", rb_coverage_start, -1); rb_define_module_function(rb_mCoverage, "resume", rb_coverage_resume, 0); rb_define_module_function(rb_mCoverage, "suspend", rb_coverage_suspend, 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, "state", rb_coverage_state, 0); rb_define_module_function(rb_mCoverage, "running?", rb_coverage_running, 0); rb_global_variable(&me2counter); }