Get SimpleCov Coverage in your Ractors with the shmactor gem
tl;dr You can get SimpleCov coverage of your Ractor code for now with the shmactor gem
I was goofin' about with ractors and noticed a surprise when running the test suite.
Actually, the story of the problem/solutions can be told from this git history:
$ git log --reverse --format='%cs %s %b' | grep -i simplecov
2026-04-08 Increase test coverage. Seems ractor code isn't always detected by Simplecov??
2026-04-13 Extract the ractor proc so we can test it directly for Simplecov Finally green!! 100% branch coverage!
2026-06-22 Introduce Shmactor to try to get Simplecov to cover lines in ractors
That's kind of like watching Ralph Wiggum's heart breaking but in reverse.
OK, let's dive into these three chapters of my life...
Chapter 1: SimpleCov does not cover lines inside the block passed to Ractor.new
So when I ran the test suite, I saw something like this:
Line Coverage: 89.63% (657 / 733)
Branch Coverage: 69.83% (81 / 116)
Line coverage (89.63%) is below the expected minimum coverage (100.00%).
Branch coverage (69.83%) is below the expected minimum coverage (100.00%).
SimpleCov failed with exit 2 due to a coverage related error
I went to look at the report to figure out what kinds of tests to add and saw this surprising output:

Whaaaa? I knew from the tests I'd already written that they couldn't possibly be
green without hitting most (if not all) of these lines.
The whole block was impossibly red. It seemed that SimpleCov wasn't counting any lines in this block
that I was passing to Ractor.new.
I have to assume this is a temporary issue and that some day it will just work. But for now I needed to come up with some sort of workaround.
Chapter 2: A functional but non-scalable solution
After about a week, I decided to solve it by moving the block to a proc assigned to a constant called RACTOR_PROC
and in the test suite adding a section that tests this proc specifically. I decided to run the proc in a thread
that has a queue. You know... an actor. I called this thread ractor_like_object and gave it a ractor-like interface to
handle the tidbits of the proc coupled to the interface of Ractor.
describe "RACTOR_PROC" do let(:ractor_like_class) do Class.new(Thread) do def queue = @queue ||= Queue.new def receive = queue.pop def send(message, move: false) = queue << message def close; end end end let(:ractor_like_object) do ractor_like_class.new do Thread.current.instance_exec(&described_class::RACTOR_PROC) end end # ...
This worked, and hence the excitement in the commit message. One strange thing, though, is that now I had to
interact directly with this ractor_like_object, building messages and sending them to it, and
interpreting the messages I get back from it. I already had code in the codebase to abstract this stuff
away. In fact, that's the whole point of the gem I was working on called
ractorize. So I was duplicating more and more of that logic
as I added increasingly complex features to the project. Eventually, to work around some memory leaks, I needed
to add two new kinds of ractors to the project.
So I had a choice to make. Should I implement both of these as procs in constants passed to Ractor.new
and figure out how to refactor my ractor_like_object to be a bit more reusable? Or maybe come up with
a deeper, more convenient workaround?
Chapter 3: A reusable thread-based actor in a gem called shmactor
What I decided to do was implement a thread-based actor in a different gem that adheres to the Ractor interface, or at least the parts of it I currently depend on.
To hook it in, what I decided to do was to have my ractors subclass from a base class called
BaseRactor.
I can switch from SimpleCov not counting lines in my ractor blocks to counting them by swapping
BaseRactor out from Ractor to Shmactor.
I need to run the suite once with BaseRactor as Ractor to verify that everything is behaviorally correct.
Then I can run it with BaseRactor as Shmactor to get a correct coverage report.
I then collate the two SimpleCov reports together for the final coverage check in the build.
This is how things look today:

This works! I don't have to duplicate any complex logic for interacting with the ractor itself in my test suite to hit all of the lines correctly. And I don't need to wire up a solution every time I need to introduce a new ractor to the project. Yay!
How to use Shmactor to get line coverage in your ractor code
There's lots of ways you could wire it up. Instead of copy/pasting stuff here, I'll just link to a couple.
You can find an RSpec usage example in its README.md here: https://github.com/ractor-shack/shmactor#usage
You can also see a less simple approach by looking at how I wire it up in the ractorize gem in a parallelized-fashion.
The semi-point-of-contact with the non-test code is constrained to src/ractorize/base_ractor.rb.
The rest of wiring it up can be seen in spec/support/shmactor.rb,
spec/support/simplecov.rb,
and Rakefile.
Interested in using Shmactor?
Hit me up if this is something that might help a project of yours while we wait for non-experimental Ractor to be released! Perhaps you're using a feature of Ractor that I'm not and you need it to be implemented in Shmactor. Or maybe the usage examples aren't working for you. Or really whatever the case may be.
Cheers!
An apology
Looking at those commit messages, it dawns on me that I've been failing to capitalize the C in SimpleCov. Whoops! Apologies to the SimpleCov family. No disrespect was intended.