ruptr 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/ruptr +10 -0
- data/lib/ruptr/adapters/assertions.rb +43 -0
- data/lib/ruptr/adapters/rr.rb +23 -0
- data/lib/ruptr/adapters/rspec_expect.rb +32 -0
- data/lib/ruptr/adapters/rspec_mocks.rb +29 -0
- data/lib/ruptr/adapters.rb +7 -0
- data/lib/ruptr/assertions.rb +493 -0
- data/lib/ruptr/autorun.rb +38 -0
- data/lib/ruptr/capture_output.rb +106 -0
- data/lib/ruptr/compat.rb +27 -0
- data/lib/ruptr/exceptions.rb +47 -0
- data/lib/ruptr/formatter.rb +78 -0
- data/lib/ruptr/golden_master.rb +143 -0
- data/lib/ruptr/instance.rb +37 -0
- data/lib/ruptr/main.rb +439 -0
- data/lib/ruptr/minitest/override.rb +4 -0
- data/lib/ruptr/minitest.rb +134 -0
- data/lib/ruptr/plain.rb +425 -0
- data/lib/ruptr/progress.rb +98 -0
- data/lib/ruptr/rake_task.rb +18 -0
- data/lib/ruptr/report.rb +104 -0
- data/lib/ruptr/result.rb +38 -0
- data/lib/ruptr/rspec/configuration.rb +191 -0
- data/lib/ruptr/rspec/example_group.rb +498 -0
- data/lib/ruptr/rspec/override.rb +4 -0
- data/lib/ruptr/rspec.rb +211 -0
- data/lib/ruptr/runner.rb +433 -0
- data/lib/ruptr/sink.rb +58 -0
- data/lib/ruptr/stringified.rb +57 -0
- data/lib/ruptr/suite.rb +188 -0
- data/lib/ruptr/surrogate_exception.rb +71 -0
- data/lib/ruptr/tabular.rb +21 -0
- data/lib/ruptr/tap.rb +51 -0
- data/lib/ruptr/testunit/override.rb +4 -0
- data/lib/ruptr/testunit.rb +117 -0
- data/lib/ruptr/timing_cache.rb +100 -0
- data/lib/ruptr/tty_colors.rb +60 -0
- data/lib/ruptr/utils.rb +57 -0
- metadata +77 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module Ruptr
|
|
7
|
+
class CaptureOutput < Delegator
|
|
8
|
+
def initialize(real_io, tls_key)
|
|
9
|
+
@real_io = real_io
|
|
10
|
+
@tls_key = tls_key
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :real_io
|
|
14
|
+
|
|
15
|
+
if Fiber.respond_to?(:[])
|
|
16
|
+
private def tls_obj = Fiber
|
|
17
|
+
else
|
|
18
|
+
private def tls_obj = Thread.current
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private def __getobj__
|
|
22
|
+
tls_obj[@tls_key] || @real_io
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def capture
|
|
26
|
+
strio = StringIO.new(+'', 'w')
|
|
27
|
+
saved = tls_obj[@tls_key]
|
|
28
|
+
begin
|
|
29
|
+
tls_obj[@tls_key] = strio
|
|
30
|
+
yield
|
|
31
|
+
strio.string
|
|
32
|
+
ensure
|
|
33
|
+
tls_obj[@tls_key] = saved
|
|
34
|
+
strio.close
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
@pinned = 0
|
|
40
|
+
@fixed = false
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
def installed? = $stdout.is_a?(self) && $stderr.is_a?(self)
|
|
44
|
+
|
|
45
|
+
def install!
|
|
46
|
+
$stdout = new($stdout, :ruptr_stdout)
|
|
47
|
+
$stderr = new($stderr, :ruptr_stderr)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def uninstall!
|
|
51
|
+
$stdout = $stdout.real_io
|
|
52
|
+
$stderr = $stderr.real_io
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reset!
|
|
56
|
+
uninstall! if @fixed || @pinned.positive?
|
|
57
|
+
@fixed = false
|
|
58
|
+
@pinned = 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fixed_install!
|
|
62
|
+
return block_given? ? yield : nil if @fixed
|
|
63
|
+
@fixed = true
|
|
64
|
+
install!
|
|
65
|
+
return unless block_given?
|
|
66
|
+
begin
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
@fixed = false
|
|
70
|
+
uninstall!
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def capture_output(&)
|
|
75
|
+
unless @fixed
|
|
76
|
+
if (ractor = defined?(::Ractor) && Ractor.current != Ractor.main)
|
|
77
|
+
install! unless installed?
|
|
78
|
+
else
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
install! unless @pinned.positive?
|
|
81
|
+
@pinned += 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
begin
|
|
86
|
+
stdout = stderr = nil
|
|
87
|
+
stderr = $stderr.capture { stdout = $stdout.capture(&) }
|
|
88
|
+
[stdout, stderr]
|
|
89
|
+
ensure
|
|
90
|
+
unless @fixed || ractor
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
@pinned -= 1
|
|
93
|
+
uninstall! unless @pinned.positive?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
module ForkHandler
|
|
101
|
+
# NOTE: This assumes that the child will exit and the parent unwinds.
|
|
102
|
+
def _fork = super.tap { |pid| CaptureOutput.reset! if pid.nil? || pid.zero? }
|
|
103
|
+
end
|
|
104
|
+
Process.singleton_class.prepend(ForkHandler)
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/ruptr/compat.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
|
|
5
|
+
module Ruptr
|
|
6
|
+
class Compat
|
|
7
|
+
def filter_test_group(tg) = tg
|
|
8
|
+
|
|
9
|
+
def adapted_test_suite
|
|
10
|
+
filter_test_group(TestSuite.new.tap { |ts| ts.add_test_subgroup(adapted_test_group) })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def global_install! = nil
|
|
14
|
+
def global_uninstall! = nil
|
|
15
|
+
def global_monkey_patch! = nil
|
|
16
|
+
def finalize_configuration! = nil
|
|
17
|
+
|
|
18
|
+
def default_project_load_paths = []
|
|
19
|
+
|
|
20
|
+
def default_project_test_globs = []
|
|
21
|
+
|
|
22
|
+
def each_default_project_test_file(project_path, &)
|
|
23
|
+
return to_enum(__method__, project_path) unless block_given?
|
|
24
|
+
project_path.glob(default_project_test_globs, &)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
module AssertionErrorMixin; end
|
|
5
|
+
|
|
6
|
+
module SkippedExceptionMixin; end
|
|
7
|
+
|
|
8
|
+
module PendingPassedMixin; end
|
|
9
|
+
|
|
10
|
+
module PendingSkippedMixin; end
|
|
11
|
+
|
|
12
|
+
module SaveMessageAsReason
|
|
13
|
+
def initialize(msg = nil)
|
|
14
|
+
super
|
|
15
|
+
@reason = msg&.to_s # preserve nil as-is (unlike #message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :reason
|
|
19
|
+
|
|
20
|
+
def reason? = !reason.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class AssertionError < StandardError
|
|
24
|
+
include AssertionErrorMixin
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class SkippedException < Exception
|
|
28
|
+
include SkippedExceptionMixin
|
|
29
|
+
include SaveMessageAsReason
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class PendingPassedError < StandardError
|
|
33
|
+
include PendingPassedMixin
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class PendingSkippedException < SkippedException
|
|
37
|
+
include PendingSkippedMixin
|
|
38
|
+
include SaveMessageAsReason
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
attr_accessor :passthrough_exceptions
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit].freeze
|
|
46
|
+
self.passthrough_exceptions = PASSTHROUGH_EXCEPTIONS
|
|
47
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'result'
|
|
5
|
+
require_relative 'sink'
|
|
6
|
+
require_relative 'tty_colors'
|
|
7
|
+
|
|
8
|
+
module Ruptr
|
|
9
|
+
class Formatter
|
|
10
|
+
class << self
|
|
11
|
+
def class_from_env(env = ENV)
|
|
12
|
+
if (s = env['RUPTR_FORMAT'])
|
|
13
|
+
find_formatter(s.to_sym) or fail "unknown formatter: #{s}"
|
|
14
|
+
else
|
|
15
|
+
require_relative 'plain'
|
|
16
|
+
Plain
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def opts_from_env(klass, env = ENV, **opts)
|
|
21
|
+
if klass.include?(Verbosity)
|
|
22
|
+
if (s = env['RUPTR_VERBOSE'])
|
|
23
|
+
opts[:verbosity] = /\A-?\d+\z/.match?(s) ? s.to_i : 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
opts
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from_env(output, env = ENV, **opts)
|
|
30
|
+
klass = class_from_env(env)
|
|
31
|
+
opts = opts_from_env(klass, env, **opts)
|
|
32
|
+
klass.new(output, **opts)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_accessor :formatter_name
|
|
36
|
+
|
|
37
|
+
def find_formatter(name)
|
|
38
|
+
traverse = proc do |c|
|
|
39
|
+
return c if c.formatter_name == name
|
|
40
|
+
c.subclasses.each(&traverse)
|
|
41
|
+
end
|
|
42
|
+
traverse.call(self)
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
include Sink
|
|
48
|
+
|
|
49
|
+
private def each_exception_cause_innermost_first(ex, &)
|
|
50
|
+
return to_enum __method__, ex unless block_given?
|
|
51
|
+
each_exception_cause_innermost_first(ex.cause, &) if ex.cause
|
|
52
|
+
yield ex
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module Colorizing
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def initialize(*args, colorizer: TTYColors::Dummy.new, **opts)
|
|
59
|
+
super(*args, **opts)
|
|
60
|
+
@colorizer = colorizer
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def colorize(s, **opts) = @colorizer.wrap(s, **opts)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
module Verbosity
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def initialize(*args, verbosity: 0, **opts)
|
|
70
|
+
super(*args, **opts)
|
|
71
|
+
@verbosity = verbosity
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def verbose?(n = 1) = @verbosity >= n
|
|
75
|
+
def quiet?(n = 1) = @verbosity <= -n
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
module Ruptr
|
|
7
|
+
class GoldenMaster
|
|
8
|
+
GOLDEN_STORE_GOLDEN_FILENAME = 'golden'
|
|
9
|
+
GOLDEN_STORE_TRIAL_FILENAME = 'trial'
|
|
10
|
+
GOLDEN_STORE_TRIAL_PRESERVE_FILENAME = 'trial-preserve'
|
|
11
|
+
|
|
12
|
+
def initialize(state_dir,
|
|
13
|
+
original_test_suite: nil,
|
|
14
|
+
filtered_test_suite: original_test_suite)
|
|
15
|
+
@state_path = Pathname(state_dir)
|
|
16
|
+
@original_test_suite = original_test_suite
|
|
17
|
+
@filtered_test_suite = filtered_test_suite
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :original_test_suite, :filtered_test_suite
|
|
21
|
+
|
|
22
|
+
def test_suite = filtered_test_suite || original_test_suite
|
|
23
|
+
|
|
24
|
+
def golden_store
|
|
25
|
+
@golden_store ||= begin
|
|
26
|
+
(@state_path / "#{GOLDEN_STORE_TRIAL_PRESERVE_FILENAME}.new").open('w') do |io|
|
|
27
|
+
Marshal.dump(
|
|
28
|
+
if !@original_test_suite
|
|
29
|
+
nil # preserve everything
|
|
30
|
+
elsif @original_test_suite.equal?(@filtered_test_suite)
|
|
31
|
+
[] # preserve nothing
|
|
32
|
+
else
|
|
33
|
+
# preserve records for test elements that have been filtered out
|
|
34
|
+
@original_test_suite.each_test_element_recursive.map(&:path_identifiers) -
|
|
35
|
+
@filtered_test_suite.each_test_element_recursive.map(&:path_identifiers)
|
|
36
|
+
end,
|
|
37
|
+
io
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
GoldenMaster::Store::FS.new(
|
|
41
|
+
golden_path: @state_path / GOLDEN_STORE_GOLDEN_FILENAME,
|
|
42
|
+
trial_path: @state_path / GOLDEN_STORE_TRIAL_FILENAME,
|
|
43
|
+
).tap(&:load_golden)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_trial!
|
|
48
|
+
return unless @golden_store
|
|
49
|
+
(@state_path / "#{GOLDEN_STORE_TRIAL_PRESERVE_FILENAME}.new")
|
|
50
|
+
.rename(@state_path / GOLDEN_STORE_TRIAL_PRESERVE_FILENAME)
|
|
51
|
+
@golden_store.dump_trial
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def accept_trial!
|
|
55
|
+
fail "no trial data" unless (@state_path / GOLDEN_STORE_TRIAL_FILENAME).exist?
|
|
56
|
+
golden_store.accept_trial(
|
|
57
|
+
preserve: (@state_path / GOLDEN_STORE_TRIAL_PRESERVE_FILENAME).open do |io|
|
|
58
|
+
v = Marshal.load(io)
|
|
59
|
+
if v.nil?
|
|
60
|
+
proc { true }
|
|
61
|
+
else
|
|
62
|
+
s = v.to_set
|
|
63
|
+
proc { |(id, _)| s.include?(id) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Store
|
|
70
|
+
def initialize
|
|
71
|
+
@golden = {}
|
|
72
|
+
@trial = {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get_golden(k, &)
|
|
76
|
+
@golden.fetch(k, &)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set_trial(k, v)
|
|
80
|
+
raise ArgumentError, "key already used: #{k.inspect}" if @trial.include?(k)
|
|
81
|
+
@trial.store(k, v)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def flush_trial = nil
|
|
85
|
+
|
|
86
|
+
def accept_trial(preserve: nil)
|
|
87
|
+
@golden.each_pair { |k, v| @trial[k] = v if !@trial.include?(k) && preserve.call(k) } if preserve
|
|
88
|
+
@golden = @trial
|
|
89
|
+
@trial = {}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class FS < self
|
|
93
|
+
# The same store must be usable from multiple forked process. For each process, trial data is
|
|
94
|
+
# accumulated in @trial and appended to the @trial_tmp_path file before the process exits.
|
|
95
|
+
|
|
96
|
+
def initialize(golden_path:, trial_path:)
|
|
97
|
+
super()
|
|
98
|
+
@golden_path = golden_path
|
|
99
|
+
@golden_tmp_path = Pathname("#{golden_path}.new")
|
|
100
|
+
@trial_path = trial_path
|
|
101
|
+
@trial_tmp_path = Pathname("#{trial_path}.new")
|
|
102
|
+
@trial_tmp_path.truncate(0) if @trial_tmp_path.exist?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_golden
|
|
106
|
+
# NOTE: The golden file is always a single Marshal chunk.
|
|
107
|
+
@golden = @golden_path.exist? ? @golden_path.open { |io| Marshal.load(io) } : {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_trial
|
|
111
|
+
# NOTE: The trial file may be made up of multiple Marshal chunks.
|
|
112
|
+
@trial = {}.tap do |h|
|
|
113
|
+
@trial_path.open { |io| h.merge!(Marshal.load(io)) until io.eof? } if @trial_path.exist?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def flush_trial
|
|
118
|
+
return if @trial.empty?
|
|
119
|
+
@trial_tmp_path.open('a') do |io|
|
|
120
|
+
# Ensuring (hopefully) that the Marshal chunk is appended with a single write(2) call.
|
|
121
|
+
io.sync = true
|
|
122
|
+
# XXX Would be better if Marshal errors were caught earlier.
|
|
123
|
+
io.write(Marshal.dump(@trial))
|
|
124
|
+
end
|
|
125
|
+
@trial.clear
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def dump_trial
|
|
129
|
+
flush_trial
|
|
130
|
+
@trial_tmp_path.rename(@trial_path) if @trial_tmp_path.exist?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def accept_trial(preserve: nil)
|
|
134
|
+
load_trial
|
|
135
|
+
super
|
|
136
|
+
@golden = @golden.to_a.sort.to_h # keep golden file identical if possible
|
|
137
|
+
@golden_tmp_path.open('w') { |io| Marshal.dump(@golden, io) }
|
|
138
|
+
@golden_tmp_path.rename(@golden_path)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
module TestInstance
|
|
5
|
+
def ruptr_initialize_test_instance(context)
|
|
6
|
+
@ruptr_context = context
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :ruptr_context
|
|
10
|
+
|
|
11
|
+
def ruptr_test_element = ruptr_context.test_element
|
|
12
|
+
|
|
13
|
+
# Common methods to let multiple assertions/expectations libraries use a shared assertions
|
|
14
|
+
# counter. The @_assertions instance variable name was chosen to be compatible with
|
|
15
|
+
# Test::Unit::Assertions::CoreAssertions which accesses it directly.
|
|
16
|
+
|
|
17
|
+
def ruptr_assertions_count
|
|
18
|
+
@_assertions || 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ruptr_assertions_count=(n)
|
|
22
|
+
@_assertions = n
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ruptr_internal_variable?(name)
|
|
26
|
+
name == :@_assertions || name == :@ruptr_context
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ruptr_wrap_test_instance
|
|
30
|
+
yield
|
|
31
|
+
ensure
|
|
32
|
+
ruptr_context.assertions_count += ruptr_assertions_count
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inspect = "#<#{self.class}: #{ruptr_context.test_element}>"
|
|
36
|
+
end
|
|
37
|
+
end
|