module Coverage

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}}}