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 4 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

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

@result = nil
self.pid = Process.pid

Expand Down
35 changes: 35 additions & 0 deletions lib/simplecov/configuration.rb
Expand Up @@ -12,6 +12,7 @@ module SimpleCov
#
module Configuration # rubocop:disable Metrics/ModuleLength
attr_writer :filters, :groups, :formatter, :print_error_status
attr_accessor :enable_for_subprocesses
robotdana marked this conversation as resolved.
Show resolved Hide resolved

#
# The root for the project. This defaults to the
Expand Down Expand Up @@ -196,6 +197,40 @@ def at_exit(&block)
@at_exit ||= proc { SimpleCov.result.format! }
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
robotdana marked this conversation as resolved.
Show resolved Hide resolved
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"