From 1488bad35b753901ebf7a0238e8a2c7302a35679 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 15 Jun 2026 20:27:55 +1200 Subject: [PATCH] Make ruby-coverage optional until capture starts Move ruby-coverage from a runtime dependency to a development dependency so applications that only load integrations such as Covered::Sus do not install the native extension unless they actually record coverage. Defer loading covered/capture from Covered::Policy#capture so requiring covered or covered/sus does not immediately require ruby/coverage. If capture is used without ruby-coverage installed, raise a targeted LoadError explaining how to add the optional dependency. Add a Sus regression test that runs in a child process with a trap ruby/coverage file earlier on the load path. Requiring covered/sus with COVERAGE unset must not trigger that file. Validation: PATH=/Users/samuel/.local/state/tec/profiles/base/current/global/bin:$PATH /Users/samuel/.local/state/tec/profiles/base/current/global/bin/bundle exec bake test Validation: /Users/samuel/.local/state/tec/profiles/base/current/global/bin/bundle exec sus test/covered/sus.rb --- covered.gemspec | 3 ++- lib/covered/capture.rb | 6 +++++- lib/covered/policy.rb | 3 ++- test/covered/sus.rb | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/covered.gemspec b/covered.gemspec index a10b22a..ad3e202 100644 --- a/covered.gemspec +++ b/covered.gemspec @@ -27,5 +27,6 @@ Gem::Specification.new do |spec| spec.add_dependency "console", "~> 1.0" spec.add_dependency "msgpack", "~> 1.0" - spec.add_dependency "ruby-coverage", "~> 0.1" + + spec.add_development_dependency "ruby-coverage", "~> 0.1" end diff --git a/lib/covered/capture.rb b/lib/covered/capture.rb index c794129..aa2d4b1 100644 --- a/lib/covered/capture.rb +++ b/lib/covered/capture.rb @@ -5,7 +5,11 @@ require_relative "wrapper" -require "ruby/coverage" +begin + require "ruby/coverage" +rescue LoadError => error + raise error.exception("Covered::Capture requires the `ruby-coverage` gem. Add `gem \"ruby-coverage\", \"~> 0.1\"` to your bundle to record coverage.") +end module Covered # Captures Ruby coverage data and forwards it to another coverage output. diff --git a/lib/covered/policy.rb b/lib/covered/policy.rb index a2817cc..9b980c7 100644 --- a/lib/covered/policy.rb +++ b/lib/covered/policy.rb @@ -5,7 +5,6 @@ require_relative "summary" require_relative "files" -require_relative "capture" require_relative "persist" require_relative "forks" @@ -67,6 +66,8 @@ def persist!(...) # The runtime capture pipeline for this policy. # @returns [Covered::Forks] The memoized capture pipeline. def capture + require_relative "capture" + @capture ||= Forks.new( Capture.new(@output) ) diff --git a/test/covered/sus.rb b/test/covered/sus.rb index b897fc9..64b8a05 100644 --- a/test/covered/sus.rb +++ b/test/covered/sus.rb @@ -5,6 +5,11 @@ require "covered/sus" +require "covered/config" + +require "fileutils" +require "tmpdir" + describe Covered::Sus do let(:coverage) do Class.new do @@ -126,4 +131,17 @@ def after_tests(assertions) expect(runner.covered).to be_nil end + + it "does not require ruby-coverage when coverage is not requested" do + Dir.mktmpdir do |root| + FileUtils.mkdir_p(File.join(root, "ruby")) + File.write(File.join(root, "ruby", "coverage.rb"), "abort 'ruby-coverage was required'") + + code = <<~RUBY + require "covered/sus" + RUBY + + expect(system({"COVERAGE" => nil, "RUBYOPT" => nil}, Gem.ruby, "-Ilib", "-I#{root}", "-e", code)).to be == true + end + end end