summaryrefslogtreecommitdiff
path: root/ext/coverage/coverage.c
diff options
context:
space:
mode:
Diffstat (limited to 'ext/coverage/coverage.c')
-rw-r--r--ext/coverage/coverage.c343
1 files changed, 226 insertions, 117 deletions
diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c
index 2f760c8eb6..1b0280b460 100644
--- a/ext/coverage/coverage.c
+++ b/ext/coverage/coverage.c
@@ -8,7 +8,7 @@
************************************************/
-#include "gc.h"
+#include "internal/gc.h"
#include "internal/hash.h"
#include "internal/thread.h"
#include "internal/sanitizers.h"
@@ -24,18 +24,57 @@ 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(:all) => nil
- * Coverage.setup(lines: bool, branches: bool, methods: bool) => nil
- * Coverage.setup(oneshot_lines: true) => nil
+ * Coverage.setup -> nil
+ * Coverage.setup(type) -> nil
+ * Coverage.setup(lines: false, branches: false, methods: false, eval: false, oneshot_lines: false) -> nil
*
- * Set up the coverage measurement.
+ * Performs setup for coverage measurement, but does not start coverage measurement.
+ * To start coverage measurement, use Coverage.resume.
*
- * Note that this method does not start the measurement itself.
- * Use Coverage.resume to start the measurement.
+ * To perform both setup and start coverage measurement, Coverage.start can be used.
*
- * You may want to use Coverage.start to setup and then start the measurement.
+ * 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@Branches+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)
@@ -53,7 +92,7 @@ rb_coverage_setup(int argc, VALUE *argv, VALUE klass)
mode = 0; /* compatible mode */
}
else if (opt == ID2SYM(rb_intern("all"))) {
- mode = COVERAGE_TARGET_LINES | COVERAGE_TARGET_BRANCHES | COVERAGE_TARGET_METHODS;
+ mode = COVERAGE_TARGET_LINES | COVERAGE_TARGET_BRANCHES | COVERAGE_TARGET_METHODS | COVERAGE_TARGET_EVAL;
}
else {
mode = 0;
@@ -71,6 +110,8 @@ rb_coverage_setup(int argc, VALUE *argv, VALUE klass)
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) {
@@ -93,7 +134,6 @@ rb_coverage_setup(int argc, VALUE *argv, VALUE klass)
rb_raise(rb_eRuntimeError, "cannot change the measuring target during coverage measurement");
}
-
return Qnil;
}
@@ -104,7 +144,7 @@ rb_coverage_setup(int argc, VALUE *argv, VALUE klass)
* Start/resume the coverage measurement.
*
* Caveat: Currently, only process-global coverage measurement is supported.
- * You cannot measure per-thread covearge. If your process has multiple thread,
+ * 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.
*/
@@ -124,14 +164,14 @@ rb_coverage_resume(VALUE klass)
/*
* call-seq:
- * Coverage.start => nil
- * Coverage.start(:all) => nil
- * Coverage.start(lines: bool, branches: bool, methods: 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.
+ * Coverage.start -> nil
+ * Coverage.start(type) -> nil
+ * Coverage.start(lines: false, branches: false, methods: false, eval: false, oneshot_lines: false) -> nil
+ *
+ * Enables coverage measurement.
+ * This method is equivalent to calling Coverage.setup with the arguments provided,
+ * and then calling Coverage.resume. See their respective documentation for more
+ * details.
*/
static VALUE
rb_coverage_start(int argc, VALUE *argv, VALUE klass)
@@ -215,8 +255,8 @@ method_coverage_i(void *vstart, void *vend, size_t stride, void *data)
VALUE ncoverages = *(VALUE*)data, v;
for (v = (VALUE)vstart; v != (VALUE)vend; v += stride) {
- void *poisoned = asan_poisoned_object_p(v);
- asan_unpoison_object(v, false);
+ 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;
@@ -259,7 +299,7 @@ method_coverage_i(void *vstart, void *vend, size_t stride, void *data)
}
if (poisoned) {
- asan_poison_object(v);
+ rb_asan_poison_object(v);
}
}
return 0;
@@ -309,7 +349,7 @@ coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h)
* 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)`.
+ * This is the same as <tt>Coverage.result(stop: false, clear: false)</tt>.
*
* {
* "file.rb" => [1, 2, nil],
@@ -324,8 +364,8 @@ rb_coverage_peek_result(VALUE klass)
if (!RTEST(coverages)) {
rb_raise(rb_eRuntimeError, "coverage measurement is not enabled");
}
- OBJ_WB_UNPROTECT(coverages);
- st_foreach(RHASH_TBL_RAW(coverages), coverage_peek_result_i, ncoverages);
+
+ rb_hash_foreach(coverages, coverage_peek_result_i, ncoverages);
if (current_mode & COVERAGE_TARGET_METHODS) {
rb_objspace_each_objects(method_coverage_i, &ncoverages);
@@ -439,156 +479,225 @@ rb_coverage_running(VALUE klass)
return current_state == RUNNING ? Qtrue : Qfalse;
}
-/* Coverage provides coverage measurement feature for Ruby.
- * This feature is experimental, so these APIs may be changed in future.
+/* \Coverage provides coverage measurement feature for Ruby.
*
- * Caveat: Currently, only process-global coverage measurement is supported.
- * You cannot measure per-thread covearge.
+ * Only process-global coverage measurement is supported, meaning
+ * that coverage cannot be measure on a per-thread basis.
*
- * = Usage
+ * = Quick Start
*
- * 1. require "coverage"
- * 2. do Coverage.start
- * 3. require or load Ruby source file
- * 4. Coverage.result will return a hash that contains filename as key and
- * coverage array as value. A coverage array gives, for each line, the
- * number of line execution by the interpreter. A +nil+ value means
- * coverage is disabled for this line (lines like +else+ and +end+).
+ * 1. Load coverage using <tt>require "coverage"</tt>.
+ * 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
*
- * [foo.rb]
- * s = 0
- * 10.times do |x|
- * s += x
- * end
+ * In file +fib.rb+:
*
- * if s == 45
- * p :ok
- * else
- * p :ng
+ * def fibonacci(n)
+ * if n == 0
+ * 0
+ * elsif n == 1
+ * 1
+ * else
+ * fibonacci(n - 1) + fibonacci(n - 2)
+ * end
* end
- * [EOF]
+ *
+ * puts fibonacci(10)
+ *
+ * In another file, coverage can be measured:
*
* require "coverage"
* Coverage.start
- * require "foo.rb"
- * p Coverage.result #=> {"foo.rb"=>[1, 1, 10, nil, nil, 1, 1, nil, 0, nil]}
+ * require "fib.rb"
+ * Coverage.result # => {"fib.rb" => [1, 177, 34, 143, 55, nil, 88, nil, nil, nil, 1]}
*
- * == Lines Coverage
+ * == Lines \Coverage
*
- * If a coverage mode is not explicitly specified when starting coverage, lines
- * coverage is what will run. It reports the number of line executions for each
- * line.
+ * 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 "foo.rb"
- * p Coverage.result #=> {"foo.rb"=>{:lines=>[1, 1, 10, nil, nil, 1, 1, nil, 0, nil]}}
+ * require "fib"
+ * Coverage.result # => {"fib.rb" => {lines: [1, 177, 34, 143, 55, nil, 88, nil, nil, nil, 1]}}
*
- * The value of the lines coverage result is an array containing how many times
- * each line was executed. Order in this array is important. For example, the
- * first item in this array, at index 0, reports how many times line 1 of this
- * file was executed while coverage was run (which, in this example, is one
- * time).
+ * The returned hash differs depending on how Coverage.setup or Coverage.start
+ * was executed.
*
- * A +nil+ value means coverage is disabled for this line (lines like +else+
- * and +end+).
+ * 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.
*
- * == Oneshot Lines Coverage
+ * If Coverage.start or Coverage.setup was called with <tt>line: true</tt>, 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.
*
- * Oneshot lines coverage tracks and reports on the executed lines while
- * coverage is running. It will not report how many times a line was executed,
- * only that it was executed.
+ * 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 "foo.rb"
- * p Coverage.result #=> {"foo.rb"=>{:oneshot_lines=>[1, 2, 3, 6, 7]}}
+ * 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
*
- * Branches coverage reports how many times each branch within each conditional
+ * Branches coverage reports the number of times each branch within each conditional
* was executed.
*
* require "coverage"
* Coverage.start(branches: true)
- * require "foo.rb"
- * p Coverage.result #=> {"foo.rb"=>{:branches=>{[:if, 0, 6, 0, 10, 3]=>{[:then, 1, 7, 2, 7, 7]=>1, [:else, 2, 9, 2, 9, 7]=>0}}}}
+ * 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 values
- * are the number of times the method was executed, and the keys are identifying
- * information about the branch.
+ * 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 each key identifying branches or conditionals
- * is the following, from left to right:
+ * 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.
+ * 1. A label for the type of branch or conditional (e.g. +:if+, +:then+, +:else+).
* 2. A unique identifier.
- * 3. The starting line number it appears on in the file.
- * 4. The starting column number it appears on in the file.
- * 5. The ending line number it appears on in the file.
- * 6. The ending column number it appears on in the file.
+ * 3. Starting line number.
+ * 4. Starting column number.
+ * 5. Ending line number.
+ * 6. Ending column number.
*
- * == Methods Coverage
+ * == Methods \Coverage
*
* Methods coverage reports how many times each method was executed.
*
- * [foo_method.rb]
- * class Greeter
- * def greet
- * "welcome!"
- * end
- * end
- *
- * def hello
- * "Hi"
- * end
- *
- * hello()
- * Greeter.new.greet()
- * [EOF]
- *
* require "coverage"
* Coverage.start(methods: true)
- * require "foo_method.rb"
- * p Coverage.result #=> {"foo_method.rb"=>{:methods=>{[Object, :hello, 7, 0, 9, 3]=>1, [Greeter, :greet, 2, 2, 4, 5]=>1}}}
+ * 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 values in this
- * hash are the number of times the method was executed, and the keys are
+ * 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. The class.
- * 2. The method name.
- * 3. The starting line number the method appears on in the file.
- * 4. The starting column number the method appears on in the file.
- * 5. The ending line number the method appears on in the file.
- * 6. The ending column number the method appears on in the file.
+ * 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.
*
- * == All Coverage Modes
+ * == Eval \Coverage
*
- * You can also run all modes of coverage simultaneously with this shortcut.
- * Note that running all coverage modes does not run both lines and oneshot
- * lines. Those modes cannot be run simultaneously. Lines coverage is run in
- * this case, because you can still use it to determine whether or not a line
- * was executed.
+ * 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 "foo.rb"
- * p Coverage.result #=> {"foo.rb"=>{:lines=>[1, 1, 10, nil, nil, 1, 1, nil, 0, nil], :branches=>{[:if, 0, 6, 0, 10, 3]=>{[:then, 1, 7, 2, 7, 7]=>1, [:else, 2, 9, 2, 9, 7]=>0}}, :methods=>{}}}
+ * 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);