ruby-changes:69413
From: Yusuke <ko1@a...>
Date: Mon, 25 Oct 2021 20:01:08 +0900 (JST)
Subject: [ruby-changes:69413] 86e3d77abb (master): Make Coverage suspendable (#4856)
https://git.ruby-lang.org/ruby.git/commit/?id=86e3d77abb From 86e3d77abb8a033650937710d1ab009e98647494 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh <mame@r...> Date: Mon, 25 Oct 2021 20:00:51 +0900 Subject: Make Coverage suspendable (#4856) * Make Coverage suspendable Add `Coverage.suspend`, `Coverage.resume` and some methods. [Feature #18176] [ruby-core:105321] --- ext/coverage/coverage.c | 123 ++++++++++++++++++++++++- spec/ruby/library/coverage/result_spec.rb | 23 ++++- test/coverage/test_coverage.rb | 146 ++++++++++++++++++++++++++++++ thread.c | 23 ++++- vm.c | 2 + vm_core.h | 4 +- 6 files changed, 305 insertions(+), 16 deletions(-) diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c index 0af5579ffc5..f948f623078 100644 --- a/ext/coverage/coverage.c +++ b/ext/coverage/coverage.c @@ -15,21 +15,38 @@ https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L15 #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.start => nil + * Coverage.setup => nil + * Coverage.setup(:all) => nil + * Coverage.setup(lines: bool, branches: bool, methods: bool) => nil + * Coverage.setup(oneshot_lines: true) => nil * - * Enables coverage measurement. + * Set up the coverage measurement. + * + * Note that this method does not start the measurement itself. + * Use Coverage.resume to start the measurement. + * + * You may want to use Coverage.start to setup and then start the measurement. */ static VALUE -rb_coverage_start(int argc, VALUE *argv, VALUE klass) +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) { @@ -70,10 +87,57 @@ rb_coverage_start(int argc, VALUE *argv, VALUE klass) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L87 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 covearge. 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) => 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; } @@ -279,6 +343,24 @@ clear_me2counter_i(VALUE key, VALUE value, VALUE unused) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L343 return ST_CONTINUE; } +/* + * call-seq: + * Coverage.suspend => nil + * + * Suspend the coverage measurement. + * You can use Coverage.resumt 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 @@ -294,6 +376,10 @@ rb_coverage_result(int argc, VALUE *argv, VALUE klass) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L376 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) { @@ -312,13 +398,34 @@ rb_coverage_result(int argc, VALUE *argv, VALUE klass) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L398 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 @@ -329,13 +436,15 @@ rb_coverage_result(int argc, VALUE *argv, VALUE klass) https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L436 static VALUE rb_coverage_running(VALUE klass) { - VALUE coverages = rb_get_coverages(); - return RTEST(coverages) ? Qtrue : Qfalse; + 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. * + * Caveat: Currently, only process-global coverage measurement is supported. + * You cannot measure per-thread covearge. + * * = Usage * * 1. require "coverage" @@ -480,9 +589,13 @@ void https://github.com/ruby/ruby/blob/trunk/ext/coverage/coverage.c#L589 Init_coverage(void) { VALUE rb_mCoverage = rb_define_module("Coverage"); + 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); } diff --git a/spec/ruby/library/coverage/result_spec.rb b/spec/ruby/library/coverage/result_spec.rb index 6cf5be13469..4cc43e8462a 100644 --- a/spec/ruby/library/coverage/result_spec.rb +++ b/spec/ruby/library/coverage/result_spec.rb @@ -65,12 +65,25 @@ describe 'Coverage.result' do https://github.com/ruby/ruby/blob/trunk/spec/ruby/library/coverage/result_spec.rb#L65 result.should == {} end - it 'second Coverage.start does nothing' do - Coverage.start - require @config_file.chomp('.rb') - result = Coverage.result + ruby_version_is ''...'3.1' do + it 'second Coverage.start does nothing' do + Coverage.start + require @config_file.chomp('.rb') + result = Coverage.result + + result.should == { @config_file => [1, 1, 1] } + end + end - result.should == { @config_file => [1, 1, 1] } + ruby_version_is '3.1' do + it 'second Coverage.start give exception' do + Coverage.start + -> { + require @config_file.chomp('.rb') + }.should raise_error(RuntimeError, 'coverage measurement is already setup') + ensure + Coverage.result + end end it 'does not include the file starting coverage since it is not tracked' do diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb index 22557bd9d8b..882368363a8 100644 --- a/test/coverage/test_coverage.rb +++ b/test/coverage/test_coverage.rb @@ -774,4 +774,150 @@ class TestCoverage < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/coverage/test_coverage.rb#L774 end end; end + + def test_coverage_suspendable + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + File.open("test.rb", "w") do |f| + f.puts <<-EOS + def foo + :ok + end + + def bar + :ok + end + + def baz + :ok + end + EOS + end + + cov1 = "[0, 0, nil, nil, 0, 1, nil, nil, 0, 0, nil]" + cov2 = "[0, 0, nil, nil, 0, 1, nil, nil, 0, 1, nil]" + assert_in_out_err(%w[-rcoverage], <<-"end;", [cov1, cov2], []) + Coverage.setup + tmp = Dir.pwd + require tmp + "/test.rb" + foo + Coverage.resume + bar + Coverage.suspend + baz + p Coverage.peek_result[tmp + "/test.rb"] + Coverage.resume + baz + p Coverage.result[tmp + "/test.rb"] + end; + + cov1 = (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/