Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subprocess handling to simplecov #881

Merged
merged 5 commits into from Mar 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,9 @@
Unreleased
==========

## Enhancements
* observe forked processes (enable with SimpleCov.enable_for_subprocesses)

0.18.5 (2020-02-25)
===================

Expand Down
47 changes: 47 additions & 0 deletions README.md
Expand Up @@ -614,6 +614,53 @@ namespace :coverage do
end
```

## Running simplecov against subprocesses

`SimpleCov.enable_for_subprocesses` will allow SimpleCov to observe subprocesses starting using `Process.fork`.
This modifies ruby's core Process.fork method so that SimpleCov can see into it, appending `" (subprocess #{pid})"`
to the `SimpleCov.command_name`, with results that can be merged together using SimpleCov's merging feature.

To configure this, use `.at_fork`.

```ruby
SimpleCov.enable_for_subprocesses true
SimpleCov.at_fork do |pid|
# This needs a unique name so it won't be ovewritten
SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
# be quiet, the parent process will be in charge of output and checking coverage totals
SimpleCov.print_error_status = false
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
SimpleCov.minimum_coverage 0
# start
SimpleCov.start
end
```

NOTE: SimpleCov must have already been started before `Process.fork` was called.

### Running simplecov against spawned subprocesses

Perhaps you're testing a ruby script with `PTY.spawn` or `Open3.popen`, or `Process.spawn` or etc.
SimpleCov can cover this too.

Add a .simplecov_spawn.rb file to your project root
```ruby
# .simplecov_spawn.rb
require 'simplecov' # this will also pick up whatever config is in .simplecov
# so ensure it just contains configuration, and doesn't call SimpleCov.start.
robotdana marked this conversation as resolved.
Show resolved Hide resolved
SimpleCov.command_name 'spawn' # As this is not for a test runner directly, script doesn't have a pre-defined base command_name
SimpleCov.at_fork.call(Process.pid) # Use the per-process setup described previously
SimpleCov.start # only now can we start.
```
Then, instead of calling your script directly, like:
```ruby
PTY.spawn('my_script.rb') do # ...
```
Use bin/ruby to require the new .simplecov_spawn file, then your script
```ruby
PTY.spawn('ruby -r./.simplecov_spawn my_script.rb') do # ...
```

## Running coverage only on demand

The Ruby STDLIB Coverage library that SimpleCov builds upon is *very* fast (on a ~10 min Rails test suite, the speed
Expand Down
37 changes: 37 additions & 0 deletions features/config_enable_for_subprocesses.feature
@@ -0,0 +1,37 @@
@rspec @process_fork

Feature:
Coverage should include code run by subprocesses

Background:
Given I'm working on the project "subprocesses"

Scenario: Coverage has seen the subprocess line
When I open the coverage report generated with `bundle exec rspec spec/simple_spec.rb`
Then I should see the groups:
| name | coverage | files |
| All Files | 100.0% | 1 |

Scenario: The at_fork proc
Given a file named ".simplecov" with:
"""
SimpleCov.enable_for_subprocesses true
SimpleCov.command_name "parent process name"
SimpleCov.at_fork do |_pid|
SimpleCov.command_name "child process name"
SimpleCov.start
end
"""
When I open the coverage report generated with `bundle exec rspec spec/simple_spec.rb`
Then I should see the groups:
| name | coverage | files |
| All Files | 100.0% | 1 |
And the report should be based upon:
| child process name |
| parent process name |

Scenario: The documentation on .simplecov_spawn
When I open the coverage report generated with `bundle exec rspec spec/spawn_spec.rb`
Then I should see the groups:
| name | coverage | files |
| All Files | 100.0% | 1 |
10 changes: 7 additions & 3 deletions features/support/env.rb
Expand Up @@ -47,9 +47,13 @@
end

Before("@rails6") do
# Rails 6 only supports Ruby 2.5+ and amazingly because string comparison
# goes beginning to end the string comparison _should_ work
skip_this_scenario if RUBY_VERSION < "2.5"
# Rails 6 only supports Ruby 2.5+
skip_this_scenario if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5")
end

Before("@process_fork") do
# Process.fork is NotImplementedError in jruby
skip_this_scenario if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
end

Aruba.configure do |config|
Expand Down
2 changes: 2 additions & 0 deletions lib/simplecov.rb
Expand Up @@ -52,6 +52,8 @@ class << self
def start(profile = nil, &block)
require "coverage"
initial_setup(profile, &block)
require_relative "./simplecov/process" if SimpleCov.enabled_for_subprocesses?

@result = nil
self.pid = Process.pid

Expand Down
46 changes: 46 additions & 0 deletions lib/simplecov/configuration.rb
Expand Up @@ -196,6 +196,52 @@ def at_exit(&block)
@at_exit ||= proc { SimpleCov.result.format! }
end

# gets or sets the enabled_for_subprocess configuration
# when true, this will inject SimpleCov code into Process.fork
def enable_for_subprocesses(value = nil)
@enable_for_subprocesses = value unless value.nil?
@enable_for_subprocesses || false
end

# gets the enabled_for_subprocess configuration
def enabled_for_subprocesses?
enable_for_subprocesses
end

#
# Gets or sets the behavior to start a new forked Process.
#
# By default, it will add " (Process #{pid})" to the command_name, and start SimpleCov in quiet mode
#
# Configure with:
#
# SimpleCov.at_fork do |pid|
# SimpleCov.start do
# # This needs a unique name so it won't be ovewritten
# SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
# # be quiet, the parent process will be in charge of using the regular formatter and checking coverage totals
# SimpleCov.print_error_status = false
# SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
# SimpleCov.minimum_coverage 0
# # start
# SimpleCov.start
# end
# end
#
def at_fork(&block)
@at_fork = block if block_given?
@at_fork ||= lambda { |pid|
# This needs a unique name so it won't be ovewritten
SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
# be quiet, the parent process will be in charge of using the regular formatter and checking coverage totals
SimpleCov.print_error_status = false
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
SimpleCov.minimum_coverage 0
robotdana marked this conversation as resolved.
Show resolved Hide resolved
# start
SimpleCov.start
}
end

#
# Returns the project name - currently assuming the last dirname in
# the SimpleCov.root is this.
Expand Down
19 changes: 19 additions & 0 deletions lib/simplecov/process.rb
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Process
class << self
def fork_with_simplecov(&block)
if defined?(SimpleCov) && SimpleCov.running
fork_without_simplecov do
SimpleCov.at_fork.call(Process.pid)
block.call if block_given?
end
else
fork_without_simplecov(&block)
end
end

alias fork_without_simplecov fork
alias fork fork_with_simplecov
end
end
5 changes: 5 additions & 0 deletions test_projects/subprocesses/.simplecov
@@ -0,0 +1,5 @@
SimpleCov.enable_for_subprocesses true
# different versions of ruby were tracking different numbers of files. idk why.
robotdana marked this conversation as resolved.
Show resolved Hide resolved
# lets only worry about one file.
SimpleCov.add_filter /command/
SimpleCov.add_filter /spawn/
5 changes: 5 additions & 0 deletions test_projects/subprocesses/.simplecov_spawn.rb
@@ -0,0 +1,5 @@
require 'bundler/setup'
require 'simplecov'
SimpleCov.command_name 'spawn'
SimpleCov.at_fork.call(Process.pid)
SimpleCov.start
7 changes: 7 additions & 0 deletions test_projects/subprocesses/lib/command
@@ -0,0 +1,7 @@
#!/usr/bin/env ruby

require_relative './subprocesses'

Subprocesses.new.run

puts 'done'
21 changes: 21 additions & 0 deletions test_projects/subprocesses/lib/subprocesses.rb
@@ -0,0 +1,21 @@
class Subprocesses
def run
method_called_in_parent_process

pid = Process.fork do
method_called_by_subprocess
end

Process.wait(pid)

true
end

def method_called_in_parent_process
true
end

def method_called_by_subprocess
true
end
end
7 changes: 7 additions & 0 deletions test_projects/subprocesses/spec/simple_spec.rb
@@ -0,0 +1,7 @@
require_relative "spec_helper"

describe Subprocesses do
it "call things" do
expect(subject.run).to be true
end
end
10 changes: 10 additions & 0 deletions test_projects/subprocesses/spec/spawn_spec.rb
@@ -0,0 +1,10 @@
require 'spec_helper'
require 'open3'
describe 'spawn' do
it 'calls things' do
Dir.chdir(File.expand_path('..', __dir__)) do
stdout, exitstatus = Open3.capture2("ruby -r./.simplecov_spawn lib/command")
expect(stdout.chomp).to eq 'done'
end
end
end
6 changes: 6 additions & 0 deletions test_projects/subprocesses/spec/spec_helper.rb
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require "simplecov"
SimpleCov.start

require_relative "../lib/subprocesses.rb"