tldr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,365 @@
1
+ # While all the methods in this file were written for TLDR, they were designed
2
+ # to maximize compatibility with minitest's assertions API and messages here:
3
+ #
4
+ # https://github.com/minitest/minitest/blob/master/lib/minitest/assertions.rb
5
+ #
6
+ # As a result, many implementations are extremely similar to those found in
7
+ # minitest. Any such implementations are Copyright © Ryan Davis, seattle.rb and
8
+ # distributed under the MIT License
9
+
10
+ require "pp"
11
+ require "super_diff"
12
+ require_relative "assertions/minitest_compatibility"
13
+
14
+ class TLDR
15
+ module Assertions
16
+ def self.h obj
17
+ obj.pretty_inspect.chomp
18
+ end
19
+
20
+ def self.msg message = nil, &default
21
+ proc {
22
+ message = message.call if Proc === message
23
+ [message.to_s, default.call].reject(&:empty?).join("\n")
24
+ }
25
+ end
26
+
27
+ def self.diff expected, actual
28
+ SuperDiff::EqualityMatchers::Main.call(expected:, actual:)
29
+ end
30
+
31
+ def self.capture_io
32
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
33
+
34
+ original_stdout, original_stderr = $stdout, $stderr
35
+ $stdout, $stderr = captured_stdout, captured_stderr
36
+
37
+ yield
38
+
39
+ [captured_stdout.string, captured_stderr.string]
40
+ ensure
41
+ $stdout = original_stdout
42
+ $stderr = original_stderr
43
+ end
44
+
45
+ def assert bool, message = nil
46
+ message ||= "Expected #{Assertions.h(bool)} to be truthy"
47
+
48
+ if bool
49
+ true
50
+ else
51
+ message = message.call if Proc === message
52
+ fail Failure, message
53
+ end
54
+ end
55
+
56
+ def refute test, message = nil
57
+ message ||= Assertions.msg(message) { "Expected #{Assertions.h(test)} to not be truthy" }
58
+ assert !test, message
59
+ end
60
+
61
+ def assert_empty obj, message = nil
62
+ message = Assertions.msg(message) {
63
+ "Expected #{Assertions.h(obj)} to be empty"
64
+ }
65
+
66
+ assert_respond_to obj, :empty?
67
+ assert obj.empty?, message
68
+ end
69
+
70
+ def refute_empty obj, message = nil
71
+ message = Assertions.msg(message) { "Expected #{Assertions.h(obj)} to not be empty" }
72
+ assert_respond_to obj, :empty?
73
+ refute obj.empty?, message
74
+ end
75
+
76
+ def assert_equal expected, actual, message = nil
77
+ message = Assertions.msg(message) { Assertions.diff expected, actual }
78
+ assert expected == actual, message
79
+ end
80
+
81
+ def refute_equal expected, actual, message = nil
82
+ message = Assertions.msg(message) {
83
+ "Expected #{Assertions.h(actual)} to not be equal to #{Assertions.h(expected)}"
84
+ }
85
+ refute expected == actual, message
86
+ end
87
+
88
+ def assert_in_delta expected, actual, delta, message = nil
89
+ difference = (expected - actual).abs
90
+ message = Assertions.msg(message) {
91
+ "Expected |#{expected} - #{actual}| (#{difference}) to be within #{delta}"
92
+ }
93
+ assert delta >= difference, message
94
+ end
95
+
96
+ def refute_in_delta expected, actual, delta = 0.001, message = nil
97
+ difference = (expected - actual).abs
98
+ message = Assertions.msg(message) {
99
+ "Expected |#{expected} - #{actual}| (#{difference}) to not be within #{delta}"
100
+ }
101
+ refute delta >= difference, message
102
+ end
103
+
104
+ def assert_in_epsilon expected, actual, epsilon = 0.001, message = nil
105
+ assert_in_delta expected, actual, [expected.abs, actual.abs].min * epsilon, message
106
+ end
107
+
108
+ def refute_in_epsilon expected, actual, epsilon = 0.001, msg = nil
109
+ refute_in_delta expected, actual, expected * epsilon, msg
110
+ end
111
+
112
+ def assert_include? expected, actual, message = nil
113
+ message = Assertions.msg(message) {
114
+ "Expected #{Assertions.h(actual)} to include #{Assertions.h(expected)}"
115
+ }
116
+ assert_respond_to actual, :include?
117
+ assert actual.include?(expected), message
118
+ end
119
+
120
+ def refute_include? expected, actual, message = nil
121
+ message = Assertions.msg(message) {
122
+ "Expected #{Assertions.h(actual)} to not include #{Assertions.h(expected)}"
123
+ }
124
+ assert_respond_to actual, :include?
125
+ refute actual.include?(expected), message
126
+ end
127
+
128
+ def assert_instance_of expected, actual, message = nil
129
+ message = Assertions.msg(message) {
130
+ "Expected #{Assertions.h(actual)} to be an instance of #{expected}, not #{actual.class}"
131
+ }
132
+ assert actual.instance_of?(expected), message
133
+ end
134
+
135
+ def refute_instance_of expected, actual, message = nil
136
+ message = Assertions.msg(message) {
137
+ "Expected #{Assertions.h(actual)} to not be an instance of #{expected}"
138
+ }
139
+ refute actual.instance_of?(expected), message
140
+ end
141
+
142
+ def assert_kind_of expected, actual, message = nil
143
+ message = Assertions.msg(message) {
144
+ "Expected #{Assertions.h(actual)} to be a kind of #{expected}, not #{actual.class}"
145
+ }
146
+ assert actual.kind_of?(expected), message # standard:disable Style/ClassCheck
147
+ end
148
+
149
+ def refute_kind_of expected, actual, message = nil
150
+ message = Assertions.msg(message) {
151
+ "Expected #{Assertions.h(actual)} to not be a kind of #{expected}"
152
+ }
153
+ refute actual.kind_of?(expected), message # standard:disable Style/ClassCheck
154
+ end
155
+
156
+ def assert_match matcher, actual, message = nil
157
+ message = Assertions.msg(message) {
158
+ "Expected #{Assertions.h(actual)} to match #{Assertions.h(matcher)}"
159
+ }
160
+ assert_respond_to matcher, :=~
161
+ matcher = Regexp.new Regexp.escape matcher if String === matcher
162
+ assert matcher =~ actual, message
163
+ Regexp.last_match
164
+ end
165
+
166
+ def refute_match matcher, actual, message = nil
167
+ message = Assertions.msg(message) {
168
+ "Expected #{Assertions.h(actual)} to not match #{Assertions.h(matcher)}"
169
+ }
170
+ assert_respond_to matcher, :=~
171
+ refute matcher =~ actual, message
172
+ end
173
+
174
+ def assert_nil obj, message = nil
175
+ message = Assertions.msg(message) {
176
+ "Expected #{Assertions.h(obj)} to be nil"
177
+ }
178
+
179
+ assert obj.nil?, message
180
+ end
181
+
182
+ def refute_nil obj, message = nil
183
+ message = Assertions.msg(message) {
184
+ "Expected #{Assertions.h(obj)} to not be nil"
185
+ }
186
+
187
+ refute obj.nil?, message
188
+ end
189
+
190
+ def assert_operator left_operand, operator, right_operand, message = nil
191
+ message = Assertions.msg(message) {
192
+ "Expected #{Assertions.h(left_operand)} to be #{operator} #{Assertions.h(right_operand)}"
193
+ }
194
+ assert left_operand.__send__(operator, right_operand), message
195
+ end
196
+
197
+ def refute_operator left_operand, operator, right_operand, message = nil
198
+ message = Assertions.msg(message) {
199
+ "Expected #{Assertions.h(left_operand)} to not be #{operator} #{Assertions.h(right_operand)}"
200
+ }
201
+ refute left_operand.__send__(operator, right_operand), message
202
+ end
203
+
204
+ def assert_output expected_stdout, expected_stderr, message = nil, &block
205
+ assert_block "assert_output requires a block to capture output" unless block
206
+
207
+ actual_stdout, actual_stderr = Assertions.capture_io(&block)
208
+
209
+ if Regexp === expected_stderr
210
+ assert_match expected_stderr, actual_stderr, "In stderr"
211
+ elsif !expected_stderr.nil?
212
+ assert_equal expected_stderr, actual_stderr, "In stderr"
213
+ end
214
+
215
+ if Regexp === expected_stdout
216
+ assert_match expected_stdout, actual_stdout, "In stdout"
217
+ elsif !expected_stdout.nil?
218
+ assert_equal expected_stdout, actual_stdout, "In stdout"
219
+ end
220
+ end
221
+
222
+ def assert_path_exists path, message = nil
223
+ message = Assertions.msg(message) {
224
+ "Expected #{Assertions.h(path)} to exist"
225
+ }
226
+
227
+ assert File.exist?(path), message
228
+ end
229
+
230
+ def refute_path_exists path, message = nil
231
+ message = Assertions.msg(message) {
232
+ "Expected #{Assertions.h(path)} to not exist"
233
+ }
234
+
235
+ refute File.exist?(path), message
236
+ end
237
+
238
+ def assert_pattern message = nil
239
+ assert false, "assert_pattern requires a block to capture errors" unless block_given?
240
+
241
+ begin
242
+ yield
243
+ rescue NoMatchingPatternError => e
244
+ assert false, Assertions.msg(message) { "Expected pattern match: #{e.message}" }
245
+ end
246
+ end
247
+
248
+ def refute_pattern message = nil
249
+ assert false, "assert_pattern requires a block to capture errors" unless block_given?
250
+
251
+ begin
252
+ yield
253
+ refute true, Assertions.msg(message) { "Expected pattern not to match, but NoMatchingPatternError was raised" }
254
+ rescue NoMatchingPatternError
255
+ end
256
+ end
257
+
258
+ def assert_predicate obj, method, message = nil
259
+ message = Assertions.msg(message) {
260
+ "Expected #{Assertions.h(obj)} to be #{method}"
261
+ }
262
+
263
+ assert obj.send(method), message
264
+ end
265
+
266
+ def refute_predicate obj, method, message = nil
267
+ message = Assertions.msg(message) {
268
+ "Expected #{Assertions.h(obj)} to not be #{method}"
269
+ }
270
+
271
+ refute obj.send(method), message
272
+ end
273
+
274
+ def assert_raises *exp
275
+ assert false, "assert_raises requires a block to capture errors" unless block_given?
276
+
277
+ message = exp.pop if String === exp.last
278
+ exp << StandardError if exp.empty?
279
+
280
+ begin
281
+ yield
282
+ rescue Failure, Skip
283
+ raise
284
+ rescue *exp => e
285
+ return e
286
+ rescue SignalException, SystemExit
287
+ raise
288
+ rescue Exception => e # standard:disable Lint/RescueException
289
+ assert false, Assertions.msg(message) {
290
+ [
291
+ "#{Assertions.h(exp)} exception expected, not",
292
+ "Class: <#{e.class}>",
293
+ "Message: <#{e.message.inspect}>",
294
+ "---Backtrace---",
295
+ TLDR.filter_backtrace(e.backtrace).join("\n"),
296
+ "---------------"
297
+ ].compact.join "\n"
298
+ }
299
+ end
300
+
301
+ exp = exp.first if exp.size == 1
302
+
303
+ assert false, "#{message}#{Assertions.h(exp)} expected but nothing was raised"
304
+ end
305
+
306
+ def assert_respond_to obj, method, message = nil
307
+ message = Assertions.msg(message) {
308
+ "Expected #{Assertions.h(obj)} (#{obj.class}) to respond to #{Assertions.h(method)}"
309
+ }
310
+
311
+ assert obj.respond_to?(method), message
312
+ end
313
+
314
+ def refute_respond_to obj, method, message = nil
315
+ message = Assertions.msg(message) {
316
+ "Expected #{Assertions.h(obj)} (#{obj.class}) to not respond to #{Assertions.h(method)}"
317
+ }
318
+
319
+ refute obj.respond_to?(method), message
320
+ end
321
+
322
+ def assert_same expected, actual, message = nil
323
+ message = Assertions.msg(message) {
324
+ <<~MSG
325
+ Expected objects to be the same, but weren't
326
+ Expected: #{Assertions.h(expected)} (oid=#{expected.object_id})
327
+ Actual: #{Assertions.h(actual)} (oid=#{actual.object_id})
328
+ MSG
329
+ }
330
+ assert expected.equal?(actual), message
331
+ end
332
+
333
+ def refute_same expected, actual, message = nil
334
+ message = Assertions.msg(message) {
335
+ "Expected #{Assertions.h(expected)} (oid=#{expected.object_id}) to not be the same as #{Assertions.h(actual)} (oid=#{actual.object_id})"
336
+ }
337
+ refute expected.equal?(actual), message
338
+ end
339
+
340
+ def assert_silent
341
+ assert_output "", "" do
342
+ yield
343
+ end
344
+ end
345
+
346
+ def assert_throws expected, message = nil
347
+ punchline = nil
348
+ caught = true
349
+ value = catch(expected) do
350
+ begin
351
+ yield
352
+ rescue ArgumentError => e
353
+ raise e unless e.message.include?("uncaught throw")
354
+ punchline = ", not #{e.message.split(" ").last}"
355
+ end
356
+ caught = false
357
+ end
358
+
359
+ assert caught, Assertions.msg(message) {
360
+ "Expected #{Assertions.h(expected)} to have been thrown#{punchline}"
361
+ }
362
+ value
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,44 @@
1
+ class TLDR
2
+ class BacktraceFilter
3
+ BASE_PATH = __dir__.freeze
4
+ SORBET_RUNTIME_PATTERN = %r{sorbet-runtime.*[/\\]lib[/\\]types[/\\]private[/\\]}
5
+ CONCURRENT_RUBY_PATTERN = %r{concurrent-ruby.*[/\\]lib[/\\]concurrent-ruby[/\\]concurrent[/\\]}
6
+
7
+ def filter backtrace
8
+ return ["No backtrace"] unless backtrace
9
+ return backtrace.dup if $DEBUG
10
+
11
+ trim_leading_frames(backtrace) ||
12
+ trim_internal_frames(backtrace) ||
13
+ backtrace.dup
14
+ end
15
+
16
+ private
17
+
18
+ def trim_leading_frames backtrace
19
+ if (trimmed = backtrace.take_while { |frame| meaningful? frame }).any?
20
+ trimmed
21
+ end
22
+ end
23
+
24
+ def trim_internal_frames backtrace
25
+ if (trimmed = backtrace.select { |frame| meaningful? frame }).any?
26
+ trimmed
27
+ end
28
+ end
29
+
30
+ def meaningful? frame
31
+ !internal? frame
32
+ end
33
+
34
+ def internal? frame
35
+ frame.start_with?(BASE_PATH) ||
36
+ frame.match?(SORBET_RUNTIME_PATTERN) ||
37
+ frame.match?(CONCURRENT_RUBY_PATTERN)
38
+ end
39
+ end
40
+
41
+ def self.filter_backtrace backtrace
42
+ BacktraceFilter.new.filter backtrace
43
+ end
44
+ end
data/lib/tldr/error.rb ADDED
@@ -0,0 +1,7 @@
1
+ class TLDR
2
+ class Error < StandardError; end
3
+
4
+ class Failure < Exception; end # standard:disable Lint/InheritException
5
+
6
+ class Skip < StandardError; end
7
+ end
@@ -0,0 +1,170 @@
1
+ require "pathname"
2
+
3
+ class TLDR
4
+ class Planner
5
+ def plan config
6
+ search_locations = expand_search_locations config.paths
7
+
8
+ prepend_load_paths config
9
+ require_test_helper config
10
+ require_tests search_locations
11
+
12
+ tests = gather_tests
13
+ config.update_after_gathering_tests! tests
14
+
15
+ Plan.new prepend(
16
+ shuffle(
17
+ exclude_by_path(
18
+ exclude_by_name(
19
+ filter_by_line(
20
+ filter_by_name(tests, config.names),
21
+ search_locations
22
+ ),
23
+ config.exclude_names
24
+ ),
25
+ config.exclude_paths
26
+ ),
27
+ config.seed
28
+ ),
29
+ config
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def expand_search_locations path_strings
36
+ path_strings.flat_map { |path_string|
37
+ File.directory?(path_string) ? Dir["#{path_string}/**/*.rb"] : path_string
38
+ }.flat_map { |path_string|
39
+ absolute_path = File.expand_path(path_string.gsub(/:.*$/, ""), Dir.pwd)
40
+ line_numbers = path_string.scan(/:(\d+)/).flatten.map(&:to_i)
41
+
42
+ if line_numbers.any?
43
+ line_numbers.map { |line_number| Location.new absolute_path, line_number }
44
+ else
45
+ [Location.new(absolute_path, nil)]
46
+ end
47
+ }.uniq
48
+ end
49
+
50
+ def gather_tests
51
+ gather_descendants(TLDR).flat_map { |subklass|
52
+ subklass.instance_methods.grep(/^test_/).sort.map { |method|
53
+ file, line = SorbetCompatibility.unwrap_method(subklass.instance_method(method)).source_location
54
+ Test.new subklass, method, file, line
55
+ }
56
+ }
57
+ end
58
+
59
+ def prepend tests, config
60
+ return tests if config.no_prepend
61
+ prepended_locations = expand_search_locations expand_globs config.prepend_tests
62
+ prepended, rest = tests.partition { |test|
63
+ locations_include_test? prepended_locations, test
64
+ }
65
+ prepended + rest
66
+ end
67
+
68
+ def shuffle tests, seed
69
+ tests.shuffle(random: Random.new(seed))
70
+ end
71
+
72
+ def exclude_by_path tests, exclude_paths
73
+ excluded_locations = expand_search_locations expand_globs exclude_paths
74
+ return tests if excluded_locations.empty?
75
+
76
+ tests.reject { |test|
77
+ locations_include_test? excluded_locations, test
78
+ }
79
+ end
80
+
81
+ def exclude_by_name tests, exclude_names
82
+ return tests if exclude_names.empty?
83
+
84
+ name_excludes = expand_names_with_patterns exclude_names
85
+
86
+ tests.reject { |test|
87
+ name_excludes.any? { |filter|
88
+ filter === test.method.to_s || filter === "#{test.klass}##{test.method}"
89
+ }
90
+ }
91
+ end
92
+
93
+ def filter_by_line tests, search_locations
94
+ line_specific_locations = search_locations.reject { |location| location.line.nil? }
95
+ return tests if line_specific_locations.empty?
96
+
97
+ tests.select { |test|
98
+ locations_include_test? line_specific_locations, test
99
+ }
100
+ end
101
+
102
+ def filter_by_name tests, names
103
+ return tests if names.empty?
104
+
105
+ name_filters = expand_names_with_patterns names
106
+
107
+ tests.select { |test|
108
+ name_filters.any? { |filter|
109
+ filter === test.method.to_s || filter === "#{test.klass}##{test.method}"
110
+ }
111
+ }
112
+ end
113
+
114
+ def prepend_load_paths config
115
+ config.load_paths.each do |load_path|
116
+ $LOAD_PATH.unshift File.expand_path(load_path, Dir.pwd)
117
+ end
118
+ end
119
+
120
+ def require_test_helper config
121
+ return if config.no_helper || config.helper.nil? || !File.exist?(config.helper)
122
+ require File.expand_path(config.helper, Dir.pwd)
123
+ end
124
+
125
+ def require_tests search_locations
126
+ search_locations.each do |location|
127
+ require location.file
128
+ end
129
+ end
130
+
131
+ def gather_descendants root_klass
132
+ root_klass.subclasses + root_klass.subclasses.flat_map { |subklass|
133
+ gather_descendants subklass
134
+ }
135
+ end
136
+
137
+ def locations_include_test? locations, test
138
+ locations.any? { |location|
139
+ location.file == test.file && (location.line.nil? || test.covers_line?(location.line))
140
+ }
141
+ end
142
+
143
+ # Because search paths to TLDR can include line numbers (e.g. a.rb:4), we
144
+ # can't just pass everything to Dir.glob. Instead, we have to check whether
145
+ # a user-provided search path looks like a glob, and if so, expand it
146
+ #
147
+ # Globby characters specified here:
148
+ # https://ruby-doc.org/3.2.2/Dir.html#method-c-glob
149
+ def expand_globs search_paths
150
+ search_paths.flat_map { |search_path|
151
+ if search_path.match?(/[*?\[\]{}]/)
152
+ raise Error, "Can't combine globs and line numbers in: #{search_path}" if search_path.match?(/:(\d+)$/)
153
+ Dir[search_path]
154
+ else
155
+ search_path
156
+ end
157
+ }
158
+ end
159
+
160
+ def expand_names_with_patterns names
161
+ names.map { |name|
162
+ if name.is_a?(String) && name =~ /^\/(.*)\/$/
163
+ Regexp.new $1
164
+ else
165
+ name
166
+ end
167
+ }
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,36 @@
1
+ class TLDR
2
+ module Reporters
3
+ class Base
4
+ def initialize(config, out = $stdout, err = $stderr)
5
+ out.sync = true
6
+ err.sync = true
7
+
8
+ @config = config
9
+ @out = out
10
+ @err = err
11
+ end
12
+
13
+ # Will be called before any tests are run
14
+ def before_suite tests
15
+ end
16
+
17
+ # Will be called after each test, unless the run has already been aborted
18
+ def after_test test_result
19
+ end
20
+
21
+ # Will be called after all tests have run, unless the run was aborted
22
+ #
23
+ # Exactly ONE of `after_suite`, `after_tldr`, or `after_fail_fast` will be called
24
+ def after_suite test_results
25
+ end
26
+
27
+ # Called after the suite-wide time limit expires and the run is aborted
28
+ def after_tldr planned_tests, wip_tests, test_results
29
+ end
30
+
31
+ # Called after the first test fails when --fail-fast is enabled, aborting the run
32
+ def after_fail_fast planned_tests, wip_tests, test_results, last_result
33
+ end
34
+ end
35
+ end
36
+ end