tldr 0.1.0

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.
@@ -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