模組 Coverage

Coverage 提供 Ruby 的涵蓋率量測功能。此功能為實驗性質,因此這些 API 可能會在未來變更。

注意事項:目前僅支援程序全域涵蓋率量測。您無法量測每個執行緒的涵蓋率。

用法

  1. require “coverage”

  2. do Coverage.start

  3. require 或載入 Ruby 原始檔

  4. Coverage.result 會傳回一個雜湊,其中包含檔案名稱作為鍵,以及涵蓋率陣列作為值。涵蓋率陣列會針對每一行提供由直譯器執行的行數。nil 值表示此行已停用涵蓋率(例如 elseend 等行)。

範例

[foo.rb]
s = 0
10.times do |x|
  s += x
end

if s == 45
  p :ok
else
  p :ng
end
[EOF]

require "coverage"
Coverage.start
require "foo.rb"
p Coverage.result  #=> {"foo.rb"=>[1, 1, 10, nil, nil, 1, 1, nil, 0, nil]}

程式碼行 Coverage

如果在開始涵蓋率時未明確指定涵蓋率模式,則會執行程式碼行涵蓋率。它會報告每一行的執行次數。

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

程式碼行涵蓋率結果的值是一個陣列,其中包含每一行執行的次數。此陣列中的順序很重要。例如,此陣列中的第一個項目(索引為 0)會報告在執行涵蓋率期間,此檔案中的第 1 行執行了幾次(在此範例中為一次)。

nil 值表示此行已停用涵蓋率(例如 elseend 等行)。

一次性程式碼行 Coverage

單次執行行數覆蓋率在覆蓋率執行期間追蹤並回報已執行的行數。它不會回報一行已執行多少次,只會回報它已執行。

require "coverage"
Coverage.start(oneshot_lines: true)
require "foo.rb"
p Coverage.result #=> {"foo.rb"=>{:oneshot_lines=>[1, 2, 3, 6, 7]}}

單次執行行數覆蓋率結果的值是一個包含已執行行號的陣列。

分支 Coverage

分支覆蓋率回報每個條件式中每個分支已執行的次數。

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

分支雜湊中的每個項目都是一個條件式,其值是另一個雜湊,其中每個項目都是該條件式中的分支。這些值是方法已執行的次數,而金鑰則是關於分支的識別資訊。

組成識別分支或條件式的每個金鑰的資訊如下,由左至右

  1. 分支或條件式的類型標籤。

  2. 一個唯一識別碼。

  3. 它出現在檔案中的起始行號。

  4. 它出現在檔案中的起始欄號。

  5. 它出現在檔案中的結束行號。

  6. 它出現在檔案中的結束欄號。

方法 Coverage

方法覆蓋率回報每個方法已執行的次數。

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

方法雜湊中的每個項目代表一個方法。此雜湊中的值是方法已執行的次數,而金鑰則是關於方法的識別資訊。

組成識別方法的每個金鑰的資訊如下,由左至右

  1. 類別。

  2. 方法名稱。

  3. 方法出現在檔案中的起始行號。

  4. 方法出現在檔案中的起始欄號。

  5. 方法出現在檔案中的結束行號。

  6. 方法出現在檔案中的結束欄號。

所有 Coverage 模式

您也可以使用此捷徑同時執行所有涵蓋模式。請注意,執行所有涵蓋模式並不會同時執行行和一次性行。這些模式無法同時執行。在此情況下,會執行行涵蓋,因為您仍可以使用它來確定是否執行某一行。

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

公用類別方法

line_stub(file) 按一下以切換來源
# File ext/coverage/lib/coverage.rb, line 4
def self.line_stub(file)
  lines = File.foreach(file).map { nil }
  iseqs = [RubyVM::InstructionSequence.compile_file(file)]
  until iseqs.empty?
    iseq = iseqs.pop
    iseq.trace_points.each {|n, type| lines[n - 1] = 0 if type == :line }
    iseq.each_child {|child| iseqs << child }
  end
  lines
end
peek_result → hash 按一下以切換來源

傳回一個雜湊,其中包含檔案名稱作為鍵和涵蓋陣列作為值。這與「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");
    }
    OBJ_WB_UNPROTECT(coverages);
    st_foreach(RHASH_TBL_RAW(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;
}
result(stop: true, clear: true) → hash 按一下以切換來源

傳回一個雜湊,其中包含檔案名稱作為鍵和涵蓋陣列作為值。如果 clear 為 true,它會將計數器清除為零。如果 stop 為 true,它會停用涵蓋量測。

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;
}
resume → nil 按一下以切換來源

開始/繼續涵蓋量測。

注意事項:目前僅支援處理序全域涵蓋量測。您無法量測每個執行緒的涵蓋。如果您的處理序有多個執行緒,使用 Coverage.resume/suspend 來擷取僅從有限程式碼區塊執行的程式碼涵蓋,可能會產生誤導性結果。

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;
}
running? → bool 按一下以切換來源

如果目前正在收集涵蓋統計資料(在 Coverage.start 呼叫之後,但在 Coverage.result 呼叫之前),則傳回 true

static VALUE
rb_coverage_running(VALUE klass)
{
    return current_state == RUNNING ? Qtrue : Qfalse;
}
setup → nil 按一下以切換來源
setup(:all) → nil
setup(lines: bool, branches: bool, methods: bool, eval: bool) → nil
setup(oneshot_lines: true) → nil

Set 設定涵蓋量測。

請注意,此方法不會啟動量測本身。使用 Coverage.resume 來啟動量測。

您可能想使用 Coverage.start 來設定,然後啟動量測。

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;
}
start → nil 按一下以切換來源
start(:all) → nil
start(lines: bool, branches: bool, methods: bool, eval: bool) → nil
start(oneshot_lines: true) → nil

啟用覆蓋率量測。詳見 Coverage 類別的文件。這等同於 Coverage.setupCoverage.resume

static VALUE
rb_coverage_start(int argc, VALUE *argv, VALUE klass)
{
    rb_coverage_setup(argc, argv, klass);
    rb_coverage_resume(klass);
    return Qnil;
}
state → :idle, :suspended, :running 按一下以切換來源

傳回覆蓋率量測的狀態。

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;
}
supported?(mode) → true 或 false 按一下以切換來源

如果給定模式支援覆蓋率量測,則傳回 true。

模式應為下列符號之一::lines:oneshot_lines:branches:methods:eval

範例

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")
    );
}
suspend → nil 按一下以切換來源

暫停覆蓋率量測。您可以使用 Coverage.resume 重新開始量測。

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