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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pp'
|
|
4
|
+
|
|
5
|
+
module Ruptr
|
|
6
|
+
# Helper to get a string representation of an arbitrary object and write it to an IO stream while
|
|
7
|
+
# attempting to deal with weird/buggy objects as well as possible.
|
|
8
|
+
class Stringified
|
|
9
|
+
def self.stringification_methods = %i[pretty_inspect inspect to_s]
|
|
10
|
+
|
|
11
|
+
def self.from(value) = value.is_a?(self) ? value : new(value)
|
|
12
|
+
|
|
13
|
+
def initialize(value)
|
|
14
|
+
# String.new is used to get an immutable snapshot of the strings and also to make sure that
|
|
15
|
+
# they are not derived classes or have instance variables that could make them unserializable.
|
|
16
|
+
@original_class_name = String.new(value.class.to_s)
|
|
17
|
+
if (@originally_a_string = value.class <= String)
|
|
18
|
+
@stringified = String.new(value)
|
|
19
|
+
else
|
|
20
|
+
if (name = self.class.stringification_methods.find { |name| value.respond_to?(name) })
|
|
21
|
+
# NOTE: String.new may call #to_s on the method's return value if needed.
|
|
22
|
+
@stringified = String.new(value.public_send(name))
|
|
23
|
+
@stringification_method = name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :original_class_name,
|
|
29
|
+
:stringified,
|
|
30
|
+
:stringification_method
|
|
31
|
+
|
|
32
|
+
def originally_a_string? = @originally_a_string
|
|
33
|
+
def stringified? = !@stringified.nil?
|
|
34
|
+
|
|
35
|
+
def compatible_with_io?(io)
|
|
36
|
+
return nil unless stringified?
|
|
37
|
+
@stringified.valid_encoding? &&
|
|
38
|
+
Encoding.compatible?(@stringified, io.external_encoding) == io.external_encoding
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private def unstringifiable_fallback = "#<#{self.class}: unstringifiable object of class #{@original_class_name}>"
|
|
42
|
+
|
|
43
|
+
def string_for_io(io)
|
|
44
|
+
case
|
|
45
|
+
when !stringified? then unstringifiable_fallback
|
|
46
|
+
when compatible_with_io?(io) then @stringified
|
|
47
|
+
else @stringified.b.inspect
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def write_to_io(io) = io.write(string_for_io(io))
|
|
52
|
+
|
|
53
|
+
def string = stringified? ? @stringified : unstringifiable_fallback
|
|
54
|
+
alias to_s string
|
|
55
|
+
alias to_str string
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/ruptr/suite.rb
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
class TestElement
|
|
5
|
+
DEFAULT_TAGS = {}.freeze
|
|
6
|
+
|
|
7
|
+
def initialize(label = nil, identifier: label, tags: DEFAULT_TAGS, &block)
|
|
8
|
+
@parent = nil
|
|
9
|
+
@label = label
|
|
10
|
+
@identifier = identifier
|
|
11
|
+
@tags = tags
|
|
12
|
+
@block = block
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_accessor :label, :identifier, :tags, :block
|
|
16
|
+
|
|
17
|
+
attr_reader :parent
|
|
18
|
+
|
|
19
|
+
def orphan? = @parent.nil?
|
|
20
|
+
|
|
21
|
+
def reparent!(new_parent)
|
|
22
|
+
fail "already has a parent" if new_parent && @parent
|
|
23
|
+
@parent = new_parent
|
|
24
|
+
@description = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def orphanize! = reparent!(nil)
|
|
28
|
+
|
|
29
|
+
def description
|
|
30
|
+
return label.nil? ? '' : label if orphan?
|
|
31
|
+
@description ||= if label.nil?
|
|
32
|
+
parent.description
|
|
33
|
+
elsif parent.description.empty?
|
|
34
|
+
label
|
|
35
|
+
else
|
|
36
|
+
"#{parent.description} #{label}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each_parent_and_self(&)
|
|
41
|
+
return to_enum __method__ unless block_given?
|
|
42
|
+
parent.each_parent_and_self(&) unless orphan?
|
|
43
|
+
yield self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def path_labels = each_parent_and_self.map(&:label)
|
|
47
|
+
def path_identifiers = each_parent_and_self.map(&:identifier)
|
|
48
|
+
|
|
49
|
+
def test_case? = false
|
|
50
|
+
def test_group? = false
|
|
51
|
+
|
|
52
|
+
def block? = !@block.nil?
|
|
53
|
+
|
|
54
|
+
def initialize_dup(_)
|
|
55
|
+
super
|
|
56
|
+
orphanize!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_s = "#<#{self.class}: #{description.inspect}>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class TestCase < TestElement
|
|
63
|
+
def test_case? = true
|
|
64
|
+
|
|
65
|
+
def run_context(context) = @block.call(context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class TestGroup < TestElement
|
|
69
|
+
def initialize(...)
|
|
70
|
+
super
|
|
71
|
+
@test_cases = []
|
|
72
|
+
@test_subgroups = []
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_group? = true
|
|
76
|
+
|
|
77
|
+
def wrap_context(context, &) = @block.call(context, &)
|
|
78
|
+
|
|
79
|
+
def each_test_case(&) = @test_cases.each(&)
|
|
80
|
+
def each_test_subgroup(&) = @test_subgroups.each(&)
|
|
81
|
+
|
|
82
|
+
def each_test_element(&)
|
|
83
|
+
return to_enum __method__ unless block_given?
|
|
84
|
+
each_test_case(&)
|
|
85
|
+
each_test_subgroup(&)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def each_test_element_recursive(&)
|
|
89
|
+
return to_enum __method__ unless block_given?
|
|
90
|
+
yield self
|
|
91
|
+
each_test_case(&)
|
|
92
|
+
each_test_subgroup { |tg| tg.each_test_element_recursive(&) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def each_test_case_recursive(&)
|
|
96
|
+
return to_enum __method__ unless block_given?
|
|
97
|
+
each_test_case(&)
|
|
98
|
+
each_test_subgroup { |tg| tg.each_test_case_recursive(&) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def each_test_group_recursive(&)
|
|
102
|
+
return to_enum __method__ unless block_given?
|
|
103
|
+
yield self
|
|
104
|
+
each_test_subgroup do |tg|
|
|
105
|
+
yield tg
|
|
106
|
+
tg.each_test_subgroup_recursive(&)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def each_test_subgroup_recursive(&)
|
|
111
|
+
return to_enum __method__ unless block_given?
|
|
112
|
+
each_test_subgroup do |tg|
|
|
113
|
+
yield tg
|
|
114
|
+
tg.each_test_subgroup_recursive(&)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def count_test_elements(&) = each_test_element_recursive.count(&)
|
|
119
|
+
def count_test_cases(&) = each_test_case_recursive.count(&)
|
|
120
|
+
def count_test_groups(&) = each_test_group_recursive.count(&)
|
|
121
|
+
def count_test_subgroups(&) = each_test_subgroup_recursive.count(&)
|
|
122
|
+
|
|
123
|
+
def empty? = @test_cases.empty? && @test_subgroups.empty?
|
|
124
|
+
|
|
125
|
+
def clear_test_cases
|
|
126
|
+
@test_cases.each(&:orphanize!)
|
|
127
|
+
@test_cases.clear
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def clear_test_subgroups
|
|
131
|
+
@test_subgroups.each(&:orphanize!)
|
|
132
|
+
@test_subgroups.clear
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def add_test_case(tc)
|
|
136
|
+
tc.reparent!(self)
|
|
137
|
+
@test_cases << tc
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def add_test_subgroup(tg)
|
|
141
|
+
tg.reparent!(self)
|
|
142
|
+
@test_subgroups << tg
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def add_test_element(te)
|
|
146
|
+
case
|
|
147
|
+
when te.test_case? then add_test_case(te)
|
|
148
|
+
when te.test_group? then add_test_subgroup(te)
|
|
149
|
+
else raise ArgumentError
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def filter_recursive!(&)
|
|
154
|
+
@test_cases.filter!(&)
|
|
155
|
+
@test_subgroups.filter! do |tg|
|
|
156
|
+
if yield tg
|
|
157
|
+
tg.filter_recursive!(&)
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def filter_recursive(&)
|
|
164
|
+
dup.tap { |tg| tg.filter_recursive!(&) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def filter_test_cases_recursive(&)
|
|
168
|
+
filter_recursive { |te| !te.test_case? || yield(te) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def initialize_dup(_)
|
|
172
|
+
super
|
|
173
|
+
@test_cases = @test_cases.map { |tc| tc.dup.tap { |tc| tc.reparent!(self) } }
|
|
174
|
+
@test_subgroups = @test_subgroups.map { |tc| tc.dup.tap { |tc| tc.reparent!(self) } }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def freeze
|
|
178
|
+
@test_cases.each(&:freeze)
|
|
179
|
+
@test_subgroups.each(&:freeze)
|
|
180
|
+
@test_cases.freeze
|
|
181
|
+
@test_subgroups.freeze
|
|
182
|
+
super
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class TestSuite < TestGroup
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
# Exception that copy some of the information of another original exception in a way that
|
|
5
|
+
# (hopefully) can be safely marshalled.
|
|
6
|
+
class SurrogateException < Exception
|
|
7
|
+
def self.detailed_message_supported? = Exception.method_defined?(:detailed_message)
|
|
8
|
+
|
|
9
|
+
def self.from_1(original_exception)
|
|
10
|
+
new(original_exception.message,
|
|
11
|
+
**if detailed_message_supported?
|
|
12
|
+
{
|
|
13
|
+
detailed_message: original_exception.detailed_message(highlight: false),
|
|
14
|
+
highlighted_detailed_message: original_exception.detailed_message(highlight: true),
|
|
15
|
+
}
|
|
16
|
+
else
|
|
17
|
+
{}
|
|
18
|
+
end,
|
|
19
|
+
original_class_name: original_exception.class.name,
|
|
20
|
+
backtrace: original_exception.backtrace)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from(original_exception)
|
|
24
|
+
return from_1(original_exception) unless original_exception.cause
|
|
25
|
+
# Recreate the cause exception chain by raising the surrogate exceptions. Simply redefining
|
|
26
|
+
# the #cause method does not work in CRuby (apparently the cause is stored in a special
|
|
27
|
+
# instance variable that is then accessed directly).
|
|
28
|
+
rec = lambda do |ex|
|
|
29
|
+
if ex.cause
|
|
30
|
+
if ex.cause.cause
|
|
31
|
+
begin
|
|
32
|
+
rec.call(ex.cause)
|
|
33
|
+
rescue self
|
|
34
|
+
raise from_1(ex)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
raise from_1(ex), cause: from_1(ex.cause)
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
raise from_1(ex), cause: nil # don't use implicit $!, if any
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
begin
|
|
44
|
+
rec.call(original_exception)
|
|
45
|
+
rescue self
|
|
46
|
+
$!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private def safe_string(v) = v.nil? ? v : String.new(v)
|
|
51
|
+
|
|
52
|
+
def initialize(message = nil,
|
|
53
|
+
detailed_message: nil, highlighted_detailed_message: nil,
|
|
54
|
+
original_class_name: nil,
|
|
55
|
+
backtrace: nil)
|
|
56
|
+
super(safe_string(message))
|
|
57
|
+
@detailed_message = safe_string(detailed_message)
|
|
58
|
+
@highlighted_detailed_message = safe_string(highlighted_detailed_message)
|
|
59
|
+
set_backtrace(backtrace)
|
|
60
|
+
@original_class_name = safe_string(original_class_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :original_class_name
|
|
64
|
+
|
|
65
|
+
if detailed_message_supported?
|
|
66
|
+
def detailed_message(highlight: false, **_)
|
|
67
|
+
(highlight ? @highlighted_detailed_message || @detailed_message : @detailed_message) || super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'formatter'
|
|
4
|
+
require_relative 'result'
|
|
5
|
+
|
|
6
|
+
module Ruptr
|
|
7
|
+
class Formatter::Tabular < Formatter
|
|
8
|
+
self.formatter_name = :tabular
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def initialize(output, **opts)
|
|
13
|
+
super(**opts)
|
|
14
|
+
@output = output
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def finish_element(te, tr)
|
|
18
|
+
@output.printf("%-7s %12.6f %s\n", tr.status, tr.processor_time || Float::NAN, te.description)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/ruptr/tap.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'sink'
|
|
4
|
+
require_relative 'formatter'
|
|
5
|
+
require_relative 'exceptions'
|
|
6
|
+
|
|
7
|
+
module Ruptr
|
|
8
|
+
class Formatter::TAP < Formatter
|
|
9
|
+
self.formatter_name = :tap
|
|
10
|
+
|
|
11
|
+
include Sink
|
|
12
|
+
|
|
13
|
+
def initialize(output, **opts)
|
|
14
|
+
super(**opts)
|
|
15
|
+
@output = output
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def begin_plan(fields)
|
|
19
|
+
@total_tests = 0
|
|
20
|
+
if (n = fields[:planned_test_case_count])
|
|
21
|
+
@output << "1..#{n}\n"
|
|
22
|
+
@emitted_plan = true
|
|
23
|
+
else
|
|
24
|
+
@emitted_plan = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finish_plan(_fields)
|
|
29
|
+
return if @emitted_plan
|
|
30
|
+
@output << "1..#{@total_tests}\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def escape(s) = s.gsub(/[\\#]/) { |m| "\\#{m}" }
|
|
34
|
+
|
|
35
|
+
def finish_element(te, tr)
|
|
36
|
+
pending = tr.skipped? && tr.exception.is_a?(PendingSkippedMixin)
|
|
37
|
+
@output << (tr.passed? || (tr.skipped? && !pending) ? "ok" : "not ok")
|
|
38
|
+
@output << ' ' << @total_tests.succ.to_s
|
|
39
|
+
@output << ' - ' << escape(te.description) if te.description
|
|
40
|
+
if tr.skipped?
|
|
41
|
+
@output << ' # ' << (pending ? "TODO" : "SKIP")
|
|
42
|
+
@output << ' ' << escape(tr.exception.message) if tr.exception&.message
|
|
43
|
+
end
|
|
44
|
+
@output << "\n"
|
|
45
|
+
if tr.failed? && tr.exception
|
|
46
|
+
tr.exception.full_message(highlight: false).each_line { |s| @output << '# ' << s }
|
|
47
|
+
end
|
|
48
|
+
@total_tests += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'suite'
|
|
4
|
+
require_relative 'compat'
|
|
5
|
+
require_relative 'autorun'
|
|
6
|
+
require_relative 'adapters/assertions'
|
|
7
|
+
|
|
8
|
+
module Ruptr
|
|
9
|
+
class Compat
|
|
10
|
+
class TestUnit < self
|
|
11
|
+
def default_project_load_paths = %w[test]
|
|
12
|
+
|
|
13
|
+
def default_project_test_globs = %w[test/**/*[-_]test.rb test/**/test[-_]*.rb]
|
|
14
|
+
|
|
15
|
+
def global_install!
|
|
16
|
+
m = if Object.const_defined?(:Test)
|
|
17
|
+
Object.const_get(:Test)
|
|
18
|
+
else
|
|
19
|
+
Object.const_set(:Test, Module.new)
|
|
20
|
+
end
|
|
21
|
+
if m.const_defined?(:Unit)
|
|
22
|
+
return if m.const_get(:Unit) == @adapter_module
|
|
23
|
+
fail "test/unit already loaded!"
|
|
24
|
+
end
|
|
25
|
+
::Test.const_set(:Unit, adapter_module)
|
|
26
|
+
this = self
|
|
27
|
+
m = Module.new do
|
|
28
|
+
define_method(:require) do |name|
|
|
29
|
+
name = name.to_path unless name.is_a?(String)
|
|
30
|
+
case name
|
|
31
|
+
when 'test/unit'
|
|
32
|
+
return
|
|
33
|
+
when 'test/unit/autorun'
|
|
34
|
+
this.schedule_autorun!
|
|
35
|
+
return
|
|
36
|
+
when 'core_assertions'
|
|
37
|
+
# Test::Unit::CoreAssertions#assert_separately spawns an interpreter with gems
|
|
38
|
+
# disabled and include path arguments based on the current $LOAD_PATH.
|
|
39
|
+
Gem.try_activate('test/unit')
|
|
40
|
+
else
|
|
41
|
+
fail "#{self.class.name}: unknown test/unit library: #{name}" if name.start_with?('test/unit/')
|
|
42
|
+
end
|
|
43
|
+
super(name)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
Kernel.prepend(m)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module DefBlockHelpers
|
|
50
|
+
def def_module(name, &) = const_set(name, Module.new(&))
|
|
51
|
+
def def_class(name, &) = const_set(name, Class.new(&))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def adapter_module
|
|
55
|
+
@adapter_module ||= Module.new do
|
|
56
|
+
extend(DefBlockHelpers)
|
|
57
|
+
|
|
58
|
+
const_set :PendedError, Assertions::SkippedException
|
|
59
|
+
const_set :AssertionFailedError, Assertions::AssertionError
|
|
60
|
+
|
|
61
|
+
def_module(:Util) do
|
|
62
|
+
extend(DefBlockHelpers)
|
|
63
|
+
def_module(:Output) do
|
|
64
|
+
def capture_output(&) = Assertions.capture_io(&)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
assertions_module = def_module(:Assertions) do
|
|
69
|
+
include(Adapters::RuptrAssertions)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def_class(:TestCase) do
|
|
73
|
+
include(TestInstance)
|
|
74
|
+
include(assertions_module)
|
|
75
|
+
|
|
76
|
+
attr_accessor :method_name
|
|
77
|
+
|
|
78
|
+
def setup = nil
|
|
79
|
+
def teardown = nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def make_run_block(klass, method_name)
|
|
85
|
+
lambda do |context|
|
|
86
|
+
inst = klass.new
|
|
87
|
+
inst.method_name = method_name
|
|
88
|
+
inst.ruptr_initialize_test_instance(context)
|
|
89
|
+
inst.ruptr_wrap_test_instance do
|
|
90
|
+
inst.setup
|
|
91
|
+
inst.public_send(method_name)
|
|
92
|
+
ensure
|
|
93
|
+
inst.teardown
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def adapted_test_group
|
|
99
|
+
traverse = lambda do |klass|
|
|
100
|
+
root = klass.equal?(adapter_module::TestCase)
|
|
101
|
+
TestGroup.new(root ? "[TestUnit]" : klass.name,
|
|
102
|
+
identifier: root ? :testunit : klass.name).tap do |tg|
|
|
103
|
+
klass.public_instance_methods(true)
|
|
104
|
+
.filter { |sym| sym.start_with?('test_') }.each do |test_method_name|
|
|
105
|
+
tc = TestCase.new(test_method_name.to_s, &make_run_block(klass, test_method_name))
|
|
106
|
+
tg.add_test_case(tc)
|
|
107
|
+
end
|
|
108
|
+
klass.subclasses.each do |subklass|
|
|
109
|
+
tg.add_test_subgroup(traverse.call(subklass))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
traverse.call(adapter_module::TestCase)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib' # for crc32
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
require_relative 'sink'
|
|
7
|
+
|
|
8
|
+
module Ruptr
|
|
9
|
+
class TimingCache
|
|
10
|
+
TIMING_CACHE_FILENAME = 'timing'
|
|
11
|
+
|
|
12
|
+
def initialize(state_dir, test_suite)
|
|
13
|
+
@state_path = Pathname(state_dir)
|
|
14
|
+
@test_suite = test_suite
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :test_suite
|
|
18
|
+
|
|
19
|
+
def timing_store
|
|
20
|
+
@timing_store ||= begin
|
|
21
|
+
@cache_path = @state_path / TIMING_CACHE_FILENAME
|
|
22
|
+
if @cache_path.exist?
|
|
23
|
+
TimingCache::Store.load(@test_suite, @cache_path)
|
|
24
|
+
else
|
|
25
|
+
TimingCache::Store.new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def save!(replace: true)
|
|
31
|
+
return unless @timing_store
|
|
32
|
+
@timing_store.dump(@test_suite, @cache_path, replace:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Store
|
|
36
|
+
def self.load(...) = new.tap { |o| o.load(...) }
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@current = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private def te_hash(te) = Zlib.crc32(te.description)
|
|
43
|
+
|
|
44
|
+
private def load_hashed(path)
|
|
45
|
+
s = Pathname(path).binread
|
|
46
|
+
n = s.length / 8
|
|
47
|
+
a = s.unpack("V#{n}e#{n}")
|
|
48
|
+
h = {}
|
|
49
|
+
n.times { |i| h[a[i]] = a[i + n] }
|
|
50
|
+
h
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load(ts, path)
|
|
54
|
+
h = load_hashed(path)
|
|
55
|
+
|
|
56
|
+
traverse = lambda do |tg|
|
|
57
|
+
total_time = 0
|
|
58
|
+
tg.each_test_case do |tc|
|
|
59
|
+
time = h[te_hash(tc)] or next
|
|
60
|
+
@current[tc] = time
|
|
61
|
+
total_time += time
|
|
62
|
+
end
|
|
63
|
+
tg.each_test_subgroup do |tg|
|
|
64
|
+
total_time += traverse.call(tg)
|
|
65
|
+
end
|
|
66
|
+
@current[tg] = total_time
|
|
67
|
+
end
|
|
68
|
+
traverse.call(ts)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def dump_hashed(path, h)
|
|
72
|
+
n = h.size
|
|
73
|
+
s = (h.keys + h.values).pack("V#{n}e#{n}")
|
|
74
|
+
Pathname(path).binwrite(s)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dump(_ts, path, replace: true)
|
|
78
|
+
h = replace ? {} : load_hashed(path)
|
|
79
|
+
@updated.each_pair do |te, time|
|
|
80
|
+
k = te_hash(te)
|
|
81
|
+
h[k] = [h[k] || 0, time].max
|
|
82
|
+
end
|
|
83
|
+
dump_hashed(path, h)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def [](te) = @current[te] || Float::INFINITY
|
|
87
|
+
|
|
88
|
+
include Sink
|
|
89
|
+
|
|
90
|
+
def begin_plan(_)
|
|
91
|
+
@updated = {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def finish_case(tc, tr)
|
|
95
|
+
time = tr.processor_time
|
|
96
|
+
@updated[tc] = time if time
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruptr
|
|
4
|
+
class TTYColors
|
|
5
|
+
def self.probably_ansi_terminal?(io) = io.tty? && !['dumb', 'unknown', ''].include?(ENV.fetch('TERM', ''))
|
|
6
|
+
def self.want_cli_colors? = ENV.include?('CLICOLOR') || ENV.include?('LS_COLORS')
|
|
7
|
+
def self.for(io) = (want_cli_colors? && probably_ansi_terminal?(io) ? ANSICodes : Dummy).new
|
|
8
|
+
|
|
9
|
+
def self.seems_to_contain_formatting_codes?(s) = s.match?(/[\e\b]/)
|
|
10
|
+
|
|
11
|
+
class Dummy < self
|
|
12
|
+
def supports?(*args) = args.empty?
|
|
13
|
+
def wrap(s, **_opts) = s.to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Overstrike < self
|
|
17
|
+
SUPPORTED = %i[bright underline].freeze
|
|
18
|
+
|
|
19
|
+
def supports?(*args) = args.all? { |name| SUPPORTED.include?(name) }
|
|
20
|
+
|
|
21
|
+
def wrap(s, bright: nil, underline: nil, **_opts)
|
|
22
|
+
s.to_s.chars.map { |c| %(#{c}#{bright ? "\b#{c}" : ''}#{underline ? "\b_" : ''}) }.join
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ANSICodes < self
|
|
27
|
+
SUPPORTED = %i[bright faint italic underline reverse strike overstrike].freeze
|
|
28
|
+
COLORS = %i[black red green yellow blue magenta cyan white].freeze
|
|
29
|
+
|
|
30
|
+
def supports?(*args)
|
|
31
|
+
args.all? do |name|
|
|
32
|
+
case name
|
|
33
|
+
when :color, :bg_color then COLORS.include?(name)
|
|
34
|
+
else SUPPORTED.include?(name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def wrap(s, **opts)
|
|
40
|
+
b = +''; e = +''
|
|
41
|
+
if opts[:bright] then b << "\e[1m"; e << "\e[22m" end
|
|
42
|
+
if opts[:faint] then b << "\e[2m"; e << "\e[22m" end
|
|
43
|
+
if opts[:italic] then b << "\e[3m"; e << "\e[23m" end
|
|
44
|
+
if opts[:underline] then b << "\e[4m"; e << "\e[24m" end
|
|
45
|
+
if opts[:reverse] then b << "\e[7m"; e << "\e[27m" end
|
|
46
|
+
if opts[:strike] then b << "\e[9m"; e << "\e[29m" end
|
|
47
|
+
if opts[:overstrike] then b << "\e[53m"; e << "\e[55m" end
|
|
48
|
+
if (color_index = COLORS.find_index(opts[:color]))
|
|
49
|
+
b << "\e[#{30 + color_index}m"
|
|
50
|
+
e << "\e[39m"
|
|
51
|
+
end
|
|
52
|
+
if (color_index = COLORS.find_index(opts[:bg_color]))
|
|
53
|
+
b << "\e[#{40 + color_index}m"
|
|
54
|
+
e << "\e[49m"
|
|
55
|
+
end
|
|
56
|
+
"#{b}#{s}#{e}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|