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 1 commit
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 (configure with SimpleCov.at_fork)

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

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

## Running simplecov against subprocesses

SimpleCov will cover code run using `Process.fork` automatically, 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.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 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
```

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 file to your project root
```ruby
# .simplecov_spawn
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.at_fork.call(Process.pid)
SimpleCov.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
1 change: 1 addition & 0 deletions lib/simplecov.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "English"
require_relative "simplecov/process"
robotdana marked this conversation as resolved.
Show resolved Hide resolved

# Coverage may be inaccurate under JRUBY.
if defined?(JRUBY_VERSION) && defined?(JRuby)
Expand Down
34 changes: 34 additions & 0 deletions lib/simplecov/configuration.rb
Expand Up @@ -196,6 +196,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 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
39 changes: 32 additions & 7 deletions spec/simplecov_spec.rb
Expand Up @@ -270,6 +270,31 @@
end
end

context "with_reports_to_clear_up" do
after do
clear_mergeable_reports
end

def a_void_method_call_that_wont_warn
true
end

it "starts coverage of subprocesses" do
SimpleCov.command_name "RSpec"
SimpleCov.start
robotdana marked this conversation as resolved.
Show resolved Hide resolved

a_void_method_call_that_wont_warn # this process

pid = Process.fork do
a_void_method_call_that_wont_warn # that process
robotdana marked this conversation as resolved.
Show resolved Hide resolved
end

Process.wait(pid) # timings!

expect(SimpleCov.result.command_name).to eq "RSpec, RSpec (subprocess: #{pid})"
end
end

context "when files to be merged" do
before do
expect(SimpleCov).to receive(:run_exit_tasks!)
Expand Down Expand Up @@ -322,14 +347,14 @@ def create_mergeable_report(name, resultset)
SimpleCov::ResultMerger.store_result(result)
FileUtils.mv resultset_path, "#{resultset_path}#{name}.final"
end
end

def clear_mergeable_reports(*names)
SimpleCov.clear_result
SimpleCov::ResultMerger.clear_resultset
FileUtils.rm resultset_path
FileUtils.rm "#{resultset_path}.lock"
names.each { |name| FileUtils.rm "#{resultset_path}#{name}.final" }
end
def clear_mergeable_reports(*names)
SimpleCov.clear_result
SimpleCov::ResultMerger.clear_resultset
FileUtils.rm resultset_path
FileUtils.rm "#{resultset_path}.lock"
names.each { |name| FileUtils.rm "#{resultset_path}#{name}.final" }
end
end

Expand Down