sspec-support 3.8.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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.md +242 -0
  3. data/LICENSE.md +23 -0
  4. data/README.md +40 -0
  5. data/lib/rspec/support.rb +149 -0
  6. data/lib/rspec/support/caller_filter.rb +83 -0
  7. data/lib/rspec/support/comparable_version.rb +46 -0
  8. data/lib/rspec/support/differ.rb +215 -0
  9. data/lib/rspec/support/directory_maker.rb +63 -0
  10. data/lib/rspec/support/encoded_string.rb +165 -0
  11. data/lib/rspec/support/fuzzy_matcher.rb +48 -0
  12. data/lib/rspec/support/hunk_generator.rb +47 -0
  13. data/lib/rspec/support/matcher_definition.rb +42 -0
  14. data/lib/rspec/support/method_signature_verifier.rb +426 -0
  15. data/lib/rspec/support/mutex.rb +73 -0
  16. data/lib/rspec/support/object_formatter.rb +275 -0
  17. data/lib/rspec/support/recursive_const_methods.rb +76 -0
  18. data/lib/rspec/support/reentrant_mutex.rb +53 -0
  19. data/lib/rspec/support/ruby_features.rb +176 -0
  20. data/lib/rspec/support/source.rb +75 -0
  21. data/lib/rspec/support/source/location.rb +21 -0
  22. data/lib/rspec/support/source/node.rb +110 -0
  23. data/lib/rspec/support/source/token.rb +87 -0
  24. data/lib/rspec/support/spec.rb +81 -0
  25. data/lib/rspec/support/spec/deprecation_helpers.rb +64 -0
  26. data/lib/rspec/support/spec/formatting_support.rb +9 -0
  27. data/lib/rspec/support/spec/in_sub_process.rb +69 -0
  28. data/lib/rspec/support/spec/library_wide_checks.rb +150 -0
  29. data/lib/rspec/support/spec/shell_out.rb +84 -0
  30. data/lib/rspec/support/spec/stderr_splitter.rb +63 -0
  31. data/lib/rspec/support/spec/string_matcher.rb +46 -0
  32. data/lib/rspec/support/spec/with_isolated_directory.rb +13 -0
  33. data/lib/rspec/support/spec/with_isolated_stderr.rb +13 -0
  34. data/lib/rspec/support/version.rb +7 -0
  35. data/lib/rspec/support/warnings.rb +39 -0
  36. metadata +115 -0
@@ -0,0 +1,48 @@
1
+ module RSpec
2
+ module Support
3
+ # Provides a means to fuzzy-match between two arbitrary objects.
4
+ # Understands array/hash nesting. Uses `===` or `==` to
5
+ # perform the matching.
6
+ module FuzzyMatcher
7
+ # @api private
8
+ def self.values_match?(expected, actual)
9
+ if Hash === actual
10
+ return hashes_match?(expected, actual) if Hash === expected
11
+ elsif Array === expected && Enumerable === actual && !(Struct === actual)
12
+ return arrays_match?(expected, actual.to_a)
13
+ end
14
+
15
+ return true if expected == actual
16
+
17
+ begin
18
+ expected === actual
19
+ rescue ArgumentError
20
+ # Some objects, like 0-arg lambdas on 1.9+, raise
21
+ # ArgumentError for `expected === actual`.
22
+ false
23
+ end
24
+ end
25
+
26
+ # @private
27
+ def self.arrays_match?(expected_list, actual_list)
28
+ return false if expected_list.size != actual_list.size
29
+
30
+ expected_list.zip(actual_list).all? do |expected, actual|
31
+ values_match?(expected, actual)
32
+ end
33
+ end
34
+
35
+ # @private
36
+ def self.hashes_match?(expected_hash, actual_hash)
37
+ return false if expected_hash.size != actual_hash.size
38
+
39
+ expected_hash.all? do |expected_key, expected_value|
40
+ actual_value = actual_hash.fetch(expected_key) { return false }
41
+ values_match?(expected_value, actual_value)
42
+ end
43
+ end
44
+
45
+ private_class_method :arrays_match?, :hashes_match?
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ require 'diff/lcs'
2
+ require 'diff/lcs/hunk'
3
+
4
+ module RSpec
5
+ module Support
6
+ # @private
7
+ class HunkGenerator
8
+ def initialize(actual, expected)
9
+ @actual = actual
10
+ @expected = expected
11
+ end
12
+
13
+ def hunks
14
+ @file_length_difference = 0
15
+ @hunks ||= diffs.map do |piece|
16
+ build_hunk(piece)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def diffs
23
+ Diff::LCS.diff(expected_lines, actual_lines)
24
+ end
25
+
26
+ def expected_lines
27
+ @expected.split("\n").map! { |e| e.chomp }
28
+ end
29
+
30
+ def actual_lines
31
+ @actual.split("\n").map! { |e| e.chomp }
32
+ end
33
+
34
+ def build_hunk(piece)
35
+ Diff::LCS::Hunk.new(
36
+ expected_lines, actual_lines, piece, context_lines, @file_length_difference
37
+ ).tap do |h|
38
+ @file_length_difference = h.file_length_difference
39
+ end
40
+ end
41
+
42
+ def context_lines
43
+ 3
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ module RSpec
2
+ module Support
3
+ # @private
4
+ def self.matcher_definitions
5
+ @matcher_definitions ||= []
6
+ end
7
+
8
+ # Used internally to break cyclic dependency between mocks, expectations,
9
+ # and support. We don't currently have a consistent implementation of our
10
+ # matchers, though we are considering changing that:
11
+ # https://github.com/rspec/rspec-mocks/issues/513
12
+ #
13
+ # @private
14
+ def self.register_matcher_definition(&block)
15
+ matcher_definitions << block
16
+ end
17
+
18
+ # Remove a previously registered matcher. Useful for cleaning up after
19
+ # yourself in specs.
20
+ #
21
+ # @private
22
+ def self.deregister_matcher_definition(&block)
23
+ matcher_definitions.delete(block)
24
+ end
25
+
26
+ # @private
27
+ def self.is_a_matcher?(object)
28
+ matcher_definitions.any? { |md| md.call(object) }
29
+ end
30
+
31
+ # @api private
32
+ #
33
+ # gives a string representation of an object for use in RSpec descriptions
34
+ def self.rspec_description_for_object(object)
35
+ if RSpec::Support.is_a_matcher?(object) && object.respond_to?(:description)
36
+ object.description
37
+ else
38
+ object
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,426 @@
1
+ require 'rspec/support'
2
+ RSpec::Support.require_rspec_support "ruby_features"
3
+ RSpec::Support.require_rspec_support "matcher_definition"
4
+
5
+ module RSpec
6
+ module Support
7
+ # Extracts info about the number of arguments and allowed/required
8
+ # keyword args of a given method.
9
+ #
10
+ # @private
11
+ class MethodSignature # rubocop:disable ClassLength
12
+ attr_reader :min_non_kw_args, :max_non_kw_args, :optional_kw_args, :required_kw_args
13
+
14
+ def initialize(method)
15
+ @method = method
16
+ @optional_kw_args = []
17
+ @required_kw_args = []
18
+ classify_parameters
19
+ end
20
+
21
+ def non_kw_args_arity_description
22
+ case max_non_kw_args
23
+ when min_non_kw_args then min_non_kw_args.to_s
24
+ when INFINITY then "#{min_non_kw_args} or more"
25
+ else "#{min_non_kw_args} to #{max_non_kw_args}"
26
+ end
27
+ end
28
+
29
+ def valid_non_kw_args?(positional_arg_count, optional_max_arg_count=positional_arg_count)
30
+ return true if positional_arg_count.nil?
31
+
32
+ min_non_kw_args <= positional_arg_count &&
33
+ optional_max_arg_count <= max_non_kw_args
34
+ end
35
+
36
+ def classify_arity(arity=@method.arity)
37
+ if arity < 0
38
+ # `~` inverts the one's complement and gives us the
39
+ # number of required args
40
+ @min_non_kw_args = ~arity
41
+ @max_non_kw_args = INFINITY
42
+ else
43
+ @min_non_kw_args = arity
44
+ @max_non_kw_args = arity
45
+ end
46
+ end
47
+
48
+ if RubyFeatures.optional_and_splat_args_supported?
49
+ def description
50
+ @description ||= begin
51
+ parts = []
52
+
53
+ unless non_kw_args_arity_description == "0"
54
+ parts << "arity of #{non_kw_args_arity_description}"
55
+ end
56
+
57
+ if @optional_kw_args.any?
58
+ parts << "optional keyword args (#{@optional_kw_args.map(&:inspect).join(", ")})"
59
+ end
60
+
61
+ if @required_kw_args.any?
62
+ parts << "required keyword args (#{@required_kw_args.map(&:inspect).join(", ")})"
63
+ end
64
+
65
+ parts << "any additional keyword args" if @allows_any_kw_args
66
+
67
+ parts.join(" and ")
68
+ end
69
+ end
70
+
71
+ def missing_kw_args_from(given_kw_args)
72
+ @required_kw_args - given_kw_args
73
+ end
74
+
75
+ def invalid_kw_args_from(given_kw_args)
76
+ return [] if @allows_any_kw_args
77
+ given_kw_args - @allowed_kw_args
78
+ end
79
+
80
+ def has_kw_args_in?(args)
81
+ Hash === args.last && could_contain_kw_args?(args)
82
+ end
83
+
84
+ # Without considering what the last arg is, could it
85
+ # contain keyword arguments?
86
+ def could_contain_kw_args?(args)
87
+ return false if args.count <= min_non_kw_args
88
+ @allows_any_kw_args || @allowed_kw_args.any?
89
+ end
90
+
91
+ def arbitrary_kw_args?
92
+ @allows_any_kw_args
93
+ end
94
+
95
+ def unlimited_args?
96
+ @max_non_kw_args == INFINITY
97
+ end
98
+
99
+ def classify_parameters
100
+ optional_non_kw_args = @min_non_kw_args = 0
101
+ @allows_any_kw_args = false
102
+
103
+ @method.parameters.each do |(type, name)|
104
+ case type
105
+ # def foo(a:)
106
+ when :keyreq then @required_kw_args << name
107
+ # def foo(a: 1)
108
+ when :key then @optional_kw_args << name
109
+ # def foo(**kw_args)
110
+ when :keyrest then @allows_any_kw_args = true
111
+ # def foo(a)
112
+ when :req then @min_non_kw_args += 1
113
+ # def foo(a = 1)
114
+ when :opt then optional_non_kw_args += 1
115
+ # def foo(*a)
116
+ when :rest then optional_non_kw_args = INFINITY
117
+ end
118
+ end
119
+
120
+ @max_non_kw_args = @min_non_kw_args + optional_non_kw_args
121
+ @allowed_kw_args = @required_kw_args + @optional_kw_args
122
+ end
123
+ else
124
+ def description
125
+ "arity of #{non_kw_args_arity_description}"
126
+ end
127
+
128
+ def missing_kw_args_from(_given_kw_args)
129
+ []
130
+ end
131
+
132
+ def invalid_kw_args_from(_given_kw_args)
133
+ []
134
+ end
135
+
136
+ def has_kw_args_in?(_args)
137
+ false
138
+ end
139
+
140
+ def could_contain_kw_args?(*)
141
+ false
142
+ end
143
+
144
+ def arbitrary_kw_args?
145
+ false
146
+ end
147
+
148
+ def unlimited_args?
149
+ false
150
+ end
151
+
152
+ alias_method :classify_parameters, :classify_arity
153
+ end
154
+
155
+ INFINITY = 1 / 0.0
156
+ end
157
+
158
+ if RSpec::Support::Ruby.jruby?
159
+ # JRuby has only partial support for UnboundMethod#parameters, so we fall back on using #arity
160
+ # https://github.com/jruby/jruby/issues/2816 and https://github.com/jruby/jruby/issues/2817
161
+ if RubyFeatures.optional_and_splat_args_supported? &&
162
+ Java::JavaLang::String.instance_method(:char_at).parameters == []
163
+
164
+ class MethodSignature < remove_const(:MethodSignature)
165
+ private
166
+
167
+ def classify_parameters
168
+ super
169
+ if (arity = @method.arity) != 0 && @method.parameters.empty?
170
+ classify_arity(arity)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # JRuby used to always report -1 arity for Java proxy methods.
177
+ # The workaround essentially makes use of Java's introspection to figure
178
+ # out matching methods (which could be more than one partly because Java
179
+ # supports multiple overloads, and partly because JRuby introduces
180
+ # aliases to make method names look more Rubyesque). If there is only a
181
+ # single match, we can use that methods arity directly instead of the
182
+ # default -1 arity.
183
+ #
184
+ # This workaround only works for Java proxy methods, and in order to
185
+ # support regular methods and blocks, we need to be careful about calling
186
+ # owner and java_class as they might not be available
187
+ if Java::JavaLang::String.instance_method(:char_at).arity == -1
188
+ class MethodSignature < remove_const(:MethodSignature)
189
+ private
190
+
191
+ def classify_parameters
192
+ super
193
+ return unless @method.arity == -1
194
+ return unless @method.respond_to?(:owner)
195
+ return unless @method.owner.respond_to?(:java_class)
196
+ java_instance_methods = @method.owner.java_class.java_instance_methods
197
+ compatible_overloads = java_instance_methods.select do |java_method|
198
+ @method == @method.owner.instance_method(java_method.name)
199
+ end
200
+ if compatible_overloads.size == 1
201
+ classify_arity(compatible_overloads.first.arity)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # Encapsulates expectations about the number of arguments and
209
+ # allowed/required keyword args of a given method.
210
+ #
211
+ # @api private
212
+ class MethodSignatureExpectation
213
+ def initialize
214
+ @min_count = nil
215
+ @max_count = nil
216
+ @keywords = []
217
+
218
+ @expect_unlimited_arguments = false
219
+ @expect_arbitrary_keywords = false
220
+ end
221
+
222
+ attr_reader :min_count, :max_count, :keywords
223
+
224
+ attr_accessor :expect_unlimited_arguments, :expect_arbitrary_keywords
225
+
226
+ def max_count=(number)
227
+ raise ArgumentError, 'must be a non-negative integer or nil' \
228
+ unless number.nil? || (number.is_a?(Integer) && number >= 0)
229
+
230
+ @max_count = number
231
+ end
232
+
233
+ def min_count=(number)
234
+ raise ArgumentError, 'must be a non-negative integer or nil' \
235
+ unless number.nil? || (number.is_a?(Integer) && number >= 0)
236
+
237
+ @min_count = number
238
+ end
239
+
240
+ def empty?
241
+ @min_count.nil? &&
242
+ @keywords.to_a.empty? &&
243
+ !@expect_arbitrary_keywords &&
244
+ !@expect_unlimited_arguments
245
+ end
246
+
247
+ def keywords=(values)
248
+ @keywords = values.to_a || []
249
+ end
250
+ end
251
+
252
+ # Deals with the slightly different semantics of block arguments.
253
+ # For methods, arguments are required unless a default value is provided.
254
+ # For blocks, arguments are optional, even if no default value is provided.
255
+ #
256
+ # However, we want to treat block args as required since you virtually
257
+ # always want to pass a value for each received argument and our
258
+ # `and_yield` has treated block args as required for many years.
259
+ #
260
+ # @api private
261
+ class BlockSignature < MethodSignature
262
+ if RubyFeatures.optional_and_splat_args_supported?
263
+ def classify_parameters
264
+ super
265
+ @min_non_kw_args = @max_non_kw_args unless @max_non_kw_args == INFINITY
266
+ end
267
+ end
268
+ end
269
+
270
+ # Abstract base class for signature verifiers.
271
+ #
272
+ # @api private
273
+ class MethodSignatureVerifier
274
+ attr_reader :non_kw_args, :kw_args, :min_non_kw_args, :max_non_kw_args
275
+
276
+ def initialize(signature, args=[])
277
+ @signature = signature
278
+ @non_kw_args, @kw_args = split_args(*args)
279
+ @min_non_kw_args = @max_non_kw_args = @non_kw_args
280
+ @arbitrary_kw_args = @unlimited_args = false
281
+ end
282
+
283
+ def with_expectation(expectation) # rubocop:disable MethodLength
284
+ return self unless MethodSignatureExpectation === expectation
285
+
286
+ if expectation.empty?
287
+ @min_non_kw_args = @max_non_kw_args = @non_kw_args = nil
288
+ @kw_args = []
289
+ else
290
+ @min_non_kw_args = @non_kw_args = expectation.min_count || 0
291
+ @max_non_kw_args = expectation.max_count || @min_non_kw_args
292
+
293
+ if RubyFeatures.optional_and_splat_args_supported?
294
+ @unlimited_args = expectation.expect_unlimited_arguments
295
+ else
296
+ @unlimited_args = false
297
+ end
298
+
299
+ if RubyFeatures.kw_args_supported?
300
+ @kw_args = expectation.keywords
301
+ @arbitrary_kw_args = expectation.expect_arbitrary_keywords
302
+ else
303
+ @kw_args = []
304
+ @arbitrary_kw_args = false
305
+ end
306
+ end
307
+
308
+ self
309
+ end
310
+
311
+ def valid?
312
+ missing_kw_args.empty? &&
313
+ invalid_kw_args.empty? &&
314
+ valid_non_kw_args? &&
315
+ arbitrary_kw_args? &&
316
+ unlimited_args?
317
+ end
318
+
319
+ def error_message
320
+ if missing_kw_args.any?
321
+ "Missing required keyword arguments: %s" % [
322
+ missing_kw_args.join(", ")
323
+ ]
324
+ elsif invalid_kw_args.any?
325
+ "Invalid keyword arguments provided: %s" % [
326
+ invalid_kw_args.join(", ")
327
+ ]
328
+ elsif !valid_non_kw_args?
329
+ "Wrong number of arguments. Expected %s, got %s." % [
330
+ @signature.non_kw_args_arity_description,
331
+ non_kw_args
332
+ ]
333
+ end
334
+ end
335
+
336
+ private
337
+
338
+ def valid_non_kw_args?
339
+ @signature.valid_non_kw_args?(min_non_kw_args, max_non_kw_args)
340
+ end
341
+
342
+ def missing_kw_args
343
+ @signature.missing_kw_args_from(kw_args)
344
+ end
345
+
346
+ def invalid_kw_args
347
+ @signature.invalid_kw_args_from(kw_args)
348
+ end
349
+
350
+ def arbitrary_kw_args?
351
+ !@arbitrary_kw_args || @signature.arbitrary_kw_args?
352
+ end
353
+
354
+ def unlimited_args?
355
+ !@unlimited_args || @signature.unlimited_args?
356
+ end
357
+
358
+ def split_args(*args)
359
+ kw_args = if @signature.has_kw_args_in?(args)
360
+ args.pop.keys
361
+ else
362
+ []
363
+ end
364
+
365
+ [args.length, kw_args]
366
+ end
367
+ end
368
+
369
+ # Figures out wether a given method can accept various arguments.
370
+ # Surprisingly non-trivial.
371
+ #
372
+ # @private
373
+ StrictSignatureVerifier = MethodSignatureVerifier
374
+
375
+ # Allows matchers to be used instead of providing keyword arguments. In
376
+ # practice, when this happens only the arity of the method is verified.
377
+ #
378
+ # @private
379
+ class LooseSignatureVerifier < MethodSignatureVerifier
380
+ private
381
+
382
+ def split_args(*args)
383
+ if RSpec::Support.is_a_matcher?(args.last) && @signature.could_contain_kw_args?(args)
384
+ args.pop
385
+ @signature = SignatureWithKeywordArgumentsMatcher.new(@signature)
386
+ end
387
+
388
+ super(*args)
389
+ end
390
+
391
+ # If a matcher is used in a signature in place of keyword arguments, all
392
+ # keyword argument validation needs to be skipped since the matcher is
393
+ # opaque.
394
+ #
395
+ # Instead, keyword arguments will be validated when the method is called
396
+ # and they are actually known.
397
+ #
398
+ # @private
399
+ class SignatureWithKeywordArgumentsMatcher
400
+ def initialize(signature)
401
+ @signature = signature
402
+ end
403
+
404
+ def missing_kw_args_from(_kw_args)
405
+ []
406
+ end
407
+
408
+ def invalid_kw_args_from(_kw_args)
409
+ []
410
+ end
411
+
412
+ def non_kw_args_arity_description
413
+ @signature.non_kw_args_arity_description
414
+ end
415
+
416
+ def valid_non_kw_args?(*args)
417
+ @signature.valid_non_kw_args?(*args)
418
+ end
419
+
420
+ def has_kw_args_in?(args)
421
+ @signature.has_kw_args_in?(args)
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end