Shmactor

Currently, Simplecov doesn't seem to cover code inside blocks passed to Ractor.new

This is a (likely temporary) gem that can be used to get around that. Shmactor is an actor implementation that has a similar interface to Ractor. But, because it uses Thread, Simplecov covers the lines of code that use it.

Installation

Typical stuff: add gem "shmactor" to your Gemfile or .gemspec file. Or even just gem install shmactor if that's your jam.

Usage

To get proper coverage stats on the code you have within ractors, you could first run your test suite with Ractor to make sure it's behaviorally correct, and then run it with Shmactor to get a coverage report.

You can switch to Shmactor using an environment variable in your test suite (or wherever) with a technique like:

# spec/shmactor.rb

if ENV["SHMACTOR"] == "true"
  require "shmactor"

  Shmactor.activate!
  BaseRactor = Shmactor
end

And in your non-test code you can do something like:

# lib/base_ractor.rb (or whatever)

BaseRactor ||= Ractor # rubocop:disable Lint/OrAssignmentToConstant

and use it like you would Ractor.new:

some_ractor = BaseRactor.new do
  typical_setup_code

  loop do
    case receive
    in :whatever, stuff
      do_something
    in :whatever_else
      do_something_else
      break
    end
  end

  some_final_value
end

To get a proper coverage report, you should run the suite once without Shmactor so you actually test the real Ractor behavior, and again with Shmactor instead to get proper code-coverage. You can stitch these together.

RSpec example

Here's an example that uses RSpec but you can of course do this however makes sense to you for the tools your using:

We will setup a file to make use of Shmactor based on an environment variable (This needs to be loaded before you load the code you are testing.):

# spec/support/simplecov.rb

require_relative "shmactor"
require "simplecov"

command_name = Shmactor.activated? ? "shmactor" : "ractor"

SimpleCov.root "#{__dir__}/../../"
SimpleCov.command_name command_name
SimpleCov.coverage_dir "coverage/#{command_name}"

SimpleCov.start do
  add_filter "spec/support/"
  enable_coverage :branch
  # This avoids printing out info about coverage generated by HTTPFormatter
  formatter Class.new { def format(_result) = nil }
end

Then we can setup running the suite both ways and stitching together the coverage report:

# Rakefile

# ...typical Rakefile stuff...

task default: [:spec, "spec:shmactor", "spec:collate_coverage", :rubocop]

desc "Run the test suite using shmactors (for line coverage on code inside ractors)"
task "spec:shmactor" do
  # It is important that we shell-out here to not interfere with the `spec` task's run
  unless system "SHMACTOR=true bundle exec rspec"
    exit $?.exitstatus
  end
end

desc "Collate coverage from all test suite runs"
task "spec:collate_coverage" do
  require "simplecov"

  SimpleCov.root __dir__

  resultsets = Dir["coverage/*/.resultset.json"]

  puts
  SimpleCov.collate resultsets do
    enable_coverage :branch
    minimum_coverage line: 100, branch: 100
  end
  puts
end

This will run the suite both ways and put the report together in the spec:collate_coverage step.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ractor-shack/shmactor

If you happen to need help with the gem please reach out!

License

This project is licensed under the MPL-2.0 license. Please see LICENSE.txt for more info.