Skip to content

Commit

Permalink
Add tests for fast fail through interrupts
Browse files Browse the repository at this point in the history
  • Loading branch information
stackmystack committed Oct 31, 2023
1 parent 6f86801 commit 01dc298
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 1 deletion.
65 changes: 65 additions & 0 deletions spec/minitest_parallel_fork_interrupt_example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
gem 'minitest'
require 'minitest/global_expectations/autorun'
require 'minitest/parallel_fork'
require 'minitest/unit'
require 'minitest/hooks/default' if ENV['MPF_MINITEST_HOOKS']

ENV['NCPU'] = '4'

# Taken and adapted from [Minitest.load_plugin](https://github.com/minitest/minitest/blob/6719ad8d8d49779669083f5029ea9a0429c49ff5/lib/minitest.rb#L108)
#
# We want to load plugins in spce/plugins and not in this Gem's lib/minitest
# because:
#
# 1. the plugin doesn't belong to the library.
# 2. the plugin doesn't behave as a proper plugin:
# a. we don't parse arguments and just load it
# b. we print to stdout and later on check the output for un/desired output
module Minitest
def self.load_mpf_plugins
return unless self.extensions.empty?

seen = {}

Dir['spec/plugins/*_plugin.rb'].each do |plugin_path|
name = File.basename plugin_path, '_plugin.rb'

next if seen[name]
seen[name] = true

require_relative plugin_path.gsub(/\Aspec\//, '')
self.extensions << name
end
end
end

Minitest.load_mpf_plugins

if ENV['MPF_FAIL_FAST']
class MyTest < MiniTest::Test
describe "failure through Interrupt" do
# We need to order the tests in order to verify the correct handling of
# the raised Interrupt by `Minitest.__run`.
# Unordered execution does not guarantee the number of un/executed tests,
# and assersions in `minitest_parallel_fork_spec.rb` would be impossible.
i_suck_and_my_tests_are_order_dependent!

parallelize_me! if ENV['MPF_PARALLELIZE_ME']

it "should fail" do
1.must_equal 2
end

it "should pass but will not" do
# We need to run a costly computation and not a `sleep`!
# Sleeping would not allow the runners to intercept the Interrupt or
# USR1 signals, and therefore we cannot reliably test for proper test
# abortion.
(1..1000000).inject(:*)
puts 'before must_equal'
1.must_equal 1
puts 'after must_equal'
end
end
end
end
22 changes: 21 additions & 1 deletion spec/minitest_parallel_fork_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
def run_mpf(*env_keys)
env_keys.each{|k| ENV[k] = '1'}
t = Time.now
output = `#{ENV['RUBY']} -I lib spec/minitest_parallel_fork_example.rb 2>&1`
prefix = env_keys.include?('MPF_FAIL_FAST') ? 'interrupt_' : ''
example = "spec/minitest_parallel_fork_#{prefix}example.rb"
output = `#{ENV['RUBY']} -I lib #{example} 2>&1`
[output, Time.now - t]
ensure
env_keys.each{|k| ENV.delete(k)}
Expand Down Expand Up @@ -63,4 +65,22 @@ def run_mpf(*env_keys)
output.must_include '20 runs, 8 assertions, 4 failures, 8 errors, 4 skip'
output.must_include 'Stats: 20R, 8A, 4F, 8E, 4S'
end

it "should stop all serial executions when and Interrupt is raised" do
output, time = run_mpf('MPF_FAIL_FAST')
time.must_be :<, 1
output.must_include 'fast_fail_plugin loaded'
output.must_include '1 runs, 1 assertions, 1 failures, 0 errors, 0 skips'
output.wont_include 'before must_equal'
output.wont_include 'after must_equal'
end

it "should stop all parallel executions when and Interrupt is raised" do
output, time = run_mpf('MPF_FAIL_FAST', 'MPF_PARALLELIZE_ME')
time.must_be :<, 1
output.must_include 'fast_fail_plugin loaded'
output.must_include '1 runs, 1 assertions, 1 failures, 0 errors, 0 skips'
output.wont_include 'before must_equal'
output.wont_include 'after must_equal'
end
end
36 changes: 36 additions & 0 deletions spec/plugins/fail_fast_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'minitest'

module Minitest
def self.plugin_fail_fast_options opts, _options
FailFastReporter.fail_fast!
puts 'fast_fail_plugin loaded'
end

def self.plugin_fail_fast_init options
if FailFastReporter.fail_fast?
io = options.fetch(:io, $stdout)
self.reporter.reporters << FailFastReporter.new(io, options)
end
end

class FailFastReporter < Reporter
def self.fail_fast!
@fail_fast = true
end

def self.fail_fast?
@fail_fast ||= false
end

def record result
if result.failures.reject { |failure| failure.is_a?(Minitest::Skip) }.any?
io.puts
raise Interrupt
else
super
end
end
end
end

0 comments on commit 01dc298

Please sign in to comment.