From e176a6b0579a8c853dd510898d2430f899a85d3b Mon Sep 17 00:00:00 2001 From: Jian Weihang Date: Thu, 16 Jul 2026 11:48:52 +0800 Subject: [PATCH] fix: merge coverage records collected across Ruby 3 and Ruby 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby 4 changed which lines `Coverage` measures: lines that Ruby 3 reported as nil (not measurable) can now carry an execution count, e.g. a literal `nil` statement. To reproduce: ``` λ cat main.rb def sum(a, b) a + b nil end sum(1, 2) λ docker run --rm -v .:/app -w /app -e RUBYLIB=lib ruby:3.3.10-trixie bash -c 'ruby -r cover_rage main.rb && bin/cover_rage -f json > cover_rage.json && paste <(ruby -r json -e "puts JSON.parse(ARGF.read)[0][%q(execution_count)].map(&:inspect)" cover_rage.json) <(ruby -r json -e "puts JSON.parse(ARGF.read)[0][%q(source)]" cover_rage.json)' sh 11 def sum(a, b) 11 a + b nil nil nil end nil 11 sum(1, 2) λ docker run --rm -v .:/app -w /app -e RUBYLIB=lib ruby:4.0.5-trixie bash -c 'ruby -r cover_rage main.rb && bin/cover_rage -f json > cover_rage.json && paste <(ruby -r json -e "puts JSON.parse(ARGF.read)[0][%q(execution_count)].map(&:inspect)" cover_rage.json) <(ruby -r json -e "puts JSON.parse(ARGF.read)[0][%q(source)]" cover_rage.json)' sh 12 def sum(a, b) 12 a + b 1 nil nil end nil 12 sum(1, 2) ``` As a result, records collected under different Ruby versions disagree on which entries of `execution_count` are nil, and merging them raised TypeError (`Integer + nil`). Resolve the mismatch in favor of the incoming record: when exactly one side is nil, `Record#+` takes the incoming record's entry — for both `execution_count` and `last_executed_at` — and the merge order in `Record.merge` is flipped so the freshly collected record wins (`existing_record + current_record`). Also rename variables in `Record.merge` for clarity: `existing` / `current` / `found` / `record` become `existing_records` / `current_records` / `existing_record` / `current_record`. --- lib/cover_rage/record.rb | 26 +++++++----- test/test_record.rb | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/lib/cover_rage/record.rb b/lib/cover_rage/record.rb index 073cfeb..3613b20 100644 --- a/lib/cover_rage/record.rb +++ b/lib/cover_rage/record.rb @@ -2,15 +2,15 @@ module CoverRage Record = Data.define(:path, :revision, :source, :execution_count, :last_executed_at) do - def self.merge(existing, current) + def self.merge(existing_records, current_records) records_to_save = [] - current.each do |record| - found = existing.find { _1.path == record.path } + current_records.each do |current_record| + existing_record = existing_records.find { _1.path == current_record.path } records_to_save << - if found.nil? || record.revision != found.revision - record + if existing_record.nil? || current_record.revision != existing_record.revision + current_record else - record + found + existing_record + current_record end end records_to_save @@ -19,13 +19,17 @@ def self.merge(existing, current) def +(other) with( execution_count: execution_count.map.with_index do |item, index| - item.nil? ? nil : item + other.execution_count[index] + other_item = other.execution_count[index] + if item.nil? && other_item.nil? then nil + elsif item.nil? || other_item.nil? then other_item + else item + other_item + end end, last_executed_at: last_executed_at.map.with_index do |item, index| - if item.nil? && other.last_executed_at[index].nil? then nil - elsif item.nil? then other.last_executed_at[index] - elsif other.last_executed_at[index].nil? then item - else [item, other.last_executed_at[index]].max + other_item = other.last_executed_at[index] + if item.nil? && other_item.nil? then nil + elsif item.nil? || other_item.nil? then other_item + else [item, other_item].max end end ) diff --git a/test/test_record.rb b/test/test_record.rb index 627a541..ba0511b 100644 --- a/test/test_record.rb +++ b/test/test_record.rb @@ -127,4 +127,92 @@ def test_sum assert_equal [3, nil], result.execution_count assert_equal [200, nil], result.last_executed_at end + + def test_sum_uses_other_when_exactly_one_execution_count_is_nil + r1 = CoverRage::Record.new( + path: '', + revision: '', + source: '', + execution_count: [1, nil], + last_executed_at: [100, nil] + ) + r2 = CoverRage::Record.new( + path: '', + revision: '', + source: '', + execution_count: [nil, 2], + last_executed_at: [nil, 200] + ) + result = r1 + r2 + assert_equal [nil, 2], result.execution_count + assert_equal [nil, 200], result.last_executed_at + end + + def test_merge_when_current_record_measures_more_lines + assert_equal( + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [3, 1], + last_executed_at: [200, 200] + ) + ], + CoverRage::Record.merge( + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [1, nil], + last_executed_at: [100, nil] + ) + ], + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [2, 1], + last_executed_at: [200, 200] + ) + ] + ) + ) + end + + def test_merge_when_current_record_measures_fewer_lines + assert_equal( + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [3, nil], + last_executed_at: [200, nil] + ) + ], + CoverRage::Record.merge( + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [1, 1], + last_executed_at: [100, 100] + ) + ], + [ + CoverRage::Record.new( + path: 'foo.rb', + revision: '1', + source: '', + execution_count: [2, nil], + last_executed_at: [200, nil] + ) + ] + ) + ) + end end