From 468adec15a33bf42ac5ed142ab7cc99eab9693fd Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 15 Jun 2026 00:00:24 +1200 Subject: [PATCH] Improve Covered self coverage --- config/covered.rb | 5 ++ test/covered/autostart.rb | 40 ++++++++++ test/covered/minitest.rb | 64 ++++++++++++++++ test/covered/partial_summary.rb | 27 +++++++ test/covered/rspec.rb | 86 +++++++++++++++++++++ test/covered/sus.rb | 129 ++++++++++++++++++++++++++++++++ 6 files changed, 351 insertions(+) create mode 100644 config/covered.rb create mode 100644 test/covered/autostart.rb create mode 100644 test/covered/sus.rb diff --git a/config/covered.rb b/config/covered.rb new file mode 100644 index 0000000..f5ec0f1 --- /dev/null +++ b/config/covered.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +def ignore_paths + super + ["bake.rb"] +end diff --git a/test/covered/autostart.rb b/test/covered/autostart.rb new file mode 100644 index 0000000..efefc2c --- /dev/null +++ b/test/covered/autostart.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "covered/config" + +describe "Coverage::Autostart" do + it "reports at exit when reports are enabled" do + events = [] + exit_hook = nil + + config = Object.new + config.define_singleton_method(:start){events << :start} + config.define_singleton_method(:finish){events << :finish} + config.define_singleton_method(:report?){true} + config.define_singleton_method(:call){|stream| events << [:call, stream]} + + Object.const_set(:Coverage, Module.new) unless Object.const_defined?(:Coverage) + ::Coverage.const_set(:Autostart, Module.new) unless ::Coverage.const_defined?(:Autostart) + + ::Coverage::Autostart.define_singleton_method(:at_exit) do |&block| + exit_hook = block + end + + mock(Covered::Config) do |mock| + mock.replace(:load){config} + end + + load File.expand_path("../../lib/covered/autostart.rb", __dir__) + + exit_hook.call + + expect(events).to be == [ + :start, + :finish, + [:call, $stderr], + ] + end +end diff --git a/test/covered/minitest.rb b/test/covered/minitest.rb index d12d07e..87cf97b 100644 --- a/test/covered/minitest.rb +++ b/test/covered/minitest.rb @@ -5,6 +5,7 @@ require "covered" require "minitest_tests" +require "covered/minitest" describe "Covered::Minitest" do include MinitestTests @@ -18,4 +19,67 @@ buffer = input.read expect(buffer).to be =~ /(.*?) files checked; (.*?) lines executed; (.*?)% covered/ end + + it "starts coverage before running minitest" do + events = [] + coverage = Object.new + coverage.define_singleton_method(:start){events << :start} + + runner = Class.new do + prepend Covered::Minitest + + def run + :ran + end + end.new + + original_coverage = $covered + $covered = coverage + + expect(runner.run).to be == :ran + expect(events).to be == [:start] + ensure + $covered = original_coverage + end + + it "registers the Minitest hooks when coverage is enabled" do + events = [] + + coverage = Object.new + coverage.define_singleton_method(:record?){true} + coverage.define_singleton_method(:finish){events << :finish} + coverage.define_singleton_method(:call){|stream| events << [:call, stream]} + + original_coverage = $covered + original_after_run = Minitest.method(:after_run) + + mock(Covered::Config) do |mock| + mock.replace(:load){coverage} + end + + mock(Minitest.singleton_class) do |mock| + mock.replace(:prepend) do |mod| + events << [:prepend, mod] + end + end + + Minitest.define_singleton_method(:after_run) do |&block| + events << :after_run + block.call + end + + events.clear + + load File.expand_path("../../lib/covered/minitest.rb", __dir__) + + expect(events).to be == [ + [:prepend, Covered::Minitest], + :after_run, + :finish, + [:call, $stderr], + ] + ensure + $covered = original_coverage + Minitest.define_singleton_method(:after_run, original_after_run) if original_after_run + end end diff --git a/test/covered/partial_summary.rb b/test/covered/partial_summary.rb index 56db95d..2ec97d7 100644 --- a/test/covered/partial_summary.rb +++ b/test/covered/partial_summary.rb @@ -54,6 +54,33 @@ expect(io.string).to be(:include?, "summary.rb") end + it "shows multiple 100% coverage files when there are partial files" do + partial_file = __FILE__ + complete_file1 = File.join(__dir__, "../covered/summary.rb") + complete_file2 = File.join(__dir__, "../covered/brief_summary.rb") + + files.mark(partial_file, 1, 1) + files.mark(partial_file, 2, 0) + + files.mark(complete_file1, 1, 1) + files.mark(complete_file2, 1, 1) + + summary.call(files, io) + + expect(io.string).to be(:include?, "2 files have 100% coverage and are not shown above:") + expect(io.string).to be(:include?, "summary.rb") + expect(io.string).to be(:include?, "brief_summary.rb") + end + + it "prints rendering errors" do + coverage = Covered::Coverage.new(Covered::Source.new("missing.rb"), [nil, 1, 0]) + files.add(coverage) + + summary.call(files, io) + + expect(io.string).to be(:include?, "Error: No such file or directory") + end + it "does not show 100% coverage files when all files are 100%" do # Create a scenario where all files have 100% coverage file1 = __FILE__ diff --git a/test/covered/rspec.rb b/test/covered/rspec.rb index 1643ce1..a607f56 100644 --- a/test/covered/rspec.rb +++ b/test/covered/rspec.rb @@ -5,6 +5,8 @@ require "covered" require "rspec_tests" +require "rspec/core" +require "covered/rspec" describe "Covered::RSpec" do include RSpecTests @@ -18,4 +20,88 @@ buffer = input.read expect(buffer).to be =~ /(.*?) files checked; (.*?) lines executed; (.*?)% covered/ end + + it "starts coverage before loading spec files" do + events = [] + coverage = Object.new + coverage.define_singleton_method(:start){events << :start} + + configuration = Class.new do + prepend Covered::RSpec::Policy + + def load_spec_files + :loaded + end + end.new + + original_coverage = $covered + $covered = coverage + + expect(configuration.load_spec_files).to be == :loaded + expect(events).to be == [:start] + ensure + $covered = original_coverage + end + + it "gets and sets the active coverage" do + configuration = Object.new + configuration.singleton_class.prepend(Covered::RSpec::Policy) + + original_coverage = $covered + coverage = Object.new + + configuration.covered = coverage + + expect(configuration.covered).to be == coverage + ensure + $covered = original_coverage + end + + it "registers the RSpec hooks when coverage is enabled" do + events = [] + output = StringIO.new + + coverage = Object.new + coverage.define_singleton_method(:record?){true} + coverage.define_singleton_method(:finish){events << :finish} + coverage.define_singleton_method(:call){|stream| events << [:call, stream]} + + config = Object.new + config.define_singleton_method(:after) do |name, &block| + events << [:after, name] + block.call + end + config.define_singleton_method(:output_stream){output} + + original_coverage = $covered + + mock(Covered::Config) do |mock| + mock.replace(:load){coverage} + end + + mock(RSpec::Core::Configuration) do |mock| + mock.replace(:prepend) do |mod| + events << [:prepend, mod] + end + end + + mock(RSpec) do |mock| + mock.replace(:configure) do |&block| + events << :configure + block.call(config) + end + end + + load File.expand_path("../../lib/covered/rspec.rb", __dir__) + + expect(events).to be == [ + [:prepend, Covered::RSpec::Policy], + :configure, + [:after, :suite], + :finish, + [:call, output], + ] + ensure + $covered = original_coverage + end end diff --git a/test/covered/sus.rb b/test/covered/sus.rb new file mode 100644 index 0000000..b897fc9 --- /dev/null +++ b/test/covered/sus.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "covered/sus" + +describe Covered::Sus do + let(:coverage) do + Class.new do + attr :events + + def initialize(record: true) + @record = record + @events = [] + end + + def record? + @record + end + + def start + @events << :start + end + + def finish + @events << :finish + end + + def call(output) + output << "covered" + @events << :call + end + end + end + + let(:runner_class) do + base = Class.new do + def initialize + end + + def after_tests(assertions) + end + end + + Class.new(base) do + include Covered::Sus + + attr :events + + def initialize + @events = [] + super + end + + def root + __dir__ + end + + def output + Struct.new(:io).new(StringIO.new) + end + + def after_tests(assertions) + @events << [:after_tests, assertions] + super + end + end + end + + it "starts and finishes configured coverage" do + config = coverage.new + + mock(Covered::Config) do |mock| + mock.replace(:load) do |root:| + expect(root).to be == __dir__ + config + end + end + + mock(ENV) do |mock| + mock.replace(:[]) do |key| + "PartialSummary" if key == "COVERAGE" + end + end + + runner = runner_class.new + runner.after_tests(:assertions) + + expect(runner.covered).to be == config + expect(config.events).to be == [:start, :finish, :call] + expect(runner.events).to be == [[:after_tests, :assertions]] + end + + it "skips coverage when it is not configured to record" do + config = coverage.new(record: false) + + mock(Covered::Config) do |mock| + mock.replace(:load) do |root:| + config + end + end + + mock(ENV) do |mock| + mock.replace(:[]) do |key| + "Quiet" if key == "COVERAGE" + end + end + + runner = runner_class.new + runner.after_tests(:assertions) + + expect(runner.covered).to be == config + expect(config.events).to be(:empty?) + end + + it "does nothing when coverage is not requested" do + mock(ENV) do |mock| + mock.replace(:[]) do |key| + nil + end + end + + runner = runner_class.new + runner.after_tests(:assertions) + + expect(runner.covered).to be_nil + end +end