Skip to content

fix: merge coverage records collected across Ruby 3 and Ruby 4 - #7

Merged
tonytonyjan merged 1 commit into
masterfrom
fix/ruby-4-coverage-merge
Jul 16, 2026
Merged

fix: merge coverage records collected across Ruby 3 and Ruby 4#7
tonytonyjan merged 1 commit into
masterfrom
fix/ruby-4-coverage-merge

Conversation

@tonytonyjan

Copy link
Copy Markdown
Collaborator

Summary

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. 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).

Reproduction

λ cat main.rb
def sum(a, b)
  a + b
  nil
end

sum(1, 2)

Ruby 3.3.10 reports the nil line as not measurable:

11      def sum(a, b)
11        a + b
nil       nil
nil     end
nil
11      sum(1, 2)

Ruby 4.0.5 counts it:

12      def sum(a, b)
12        a + b
1         nil
nil     end
nil
12      sum(1, 2)

Fix

Resolve the mismatch in favor of the incoming record:

  • Record#+ now falls back to other's entry when exactly one side is nil.
  • The merge order in Record.merge is flipped from record + found to found + record so the freshly collected record wins.

Tests

Three tests added to test/test_record.rb, mutation-checked so that reverting either half of the fix independently makes them fail:

  • test_sum_uses_other_when_exactly_one_execution_count_is_nil — pins the nil-fallback in Record#+ (raised TypeError before the fix).
  • test_merge_when_current_record_measures_more_lines — a stored Ruby-3-shaped record merged with a fresh Ruby-4-shaped one keeps the new line's count.
  • test_merge_when_current_record_measures_fewer_lines — the reverse direction, pinning the found + record order flip.

Full suite passes: TEST_REDIS_URL=redis://localhost:6379 bundle exec rake test → 28 runs, 33 assertions, 0 failures, 0 errors.

🤖 Generated with Claude Code

Comment thread lib/cover_rage/record.rb
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

@requiemformemories requiemformemories Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should we merge the data using the same logic that last_executed_at is using?

For example, if other_item is nil but item is 1 (somehow the unused code is now used), should it be item instead of other_item?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment, I've made the change 👍

@choznerol choznerol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on Fumi's question

Comment thread lib/cover_rage/record.rb Outdated
record
else
record + found
found + record

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought(non-blocking): Unrelated to this PR, but I found it challenging to keep track of existing v.s. current v.s. record v.s. found at the same time.

WDYT about:

-existing
+existing_records
-found
+existing_record
-current
+current_records
-record
+current_record

This way, instead of:

-            record + found
+            found + record

the PR diff would be:

-            current_record + existing_record
+            existing_record + current_record

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed variables. Let me know if it's clearer to you 🙏

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. Thanks you 🙇

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`.

@choznerol choznerol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you!

@requiemformemories requiemformemories left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@tonytonyjan
tonytonyjan merged commit 2fc94de into master Jul 16, 2026
4 checks passed
@tonytonyjan
tonytonyjan deleted the fix/ruby-4-coverage-merge branch July 16, 2026 06:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants