rspec-support 3.0.4 → 3.12.1

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 (41) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/Changelog.md +322 -0
  4. data/{LICENSE.txt → LICENSE.md} +3 -2
  5. data/README.md +29 -6
  6. data/lib/rspec/support/caller_filter.rb +35 -16
  7. data/lib/rspec/support/comparable_version.rb +46 -0
  8. data/lib/rspec/support/differ.rb +51 -41
  9. data/lib/rspec/support/directory_maker.rb +63 -0
  10. data/lib/rspec/support/encoded_string.rb +110 -15
  11. data/lib/rspec/support/fuzzy_matcher.rb +5 -6
  12. data/lib/rspec/support/hunk_generator.rb +0 -1
  13. data/lib/rspec/support/matcher_definition.rb +42 -0
  14. data/lib/rspec/support/method_signature_verifier.rb +287 -54
  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 +78 -0
  19. data/lib/rspec/support/ruby_features.rb +177 -14
  20. data/lib/rspec/support/source/location.rb +21 -0
  21. data/lib/rspec/support/source/node.rb +110 -0
  22. data/lib/rspec/support/source/token.rb +94 -0
  23. data/lib/rspec/support/source.rb +85 -0
  24. data/lib/rspec/support/spec/deprecation_helpers.rb +19 -32
  25. data/lib/rspec/support/spec/diff_helpers.rb +31 -0
  26. data/lib/rspec/support/spec/in_sub_process.rb +43 -16
  27. data/lib/rspec/support/spec/library_wide_checks.rb +150 -0
  28. data/lib/rspec/support/spec/shell_out.rb +108 -0
  29. data/lib/rspec/support/spec/stderr_splitter.rb +31 -9
  30. data/lib/rspec/support/spec/string_matcher.rb +45 -0
  31. data/lib/rspec/support/spec/with_isolated_directory.rb +13 -0
  32. data/lib/rspec/support/spec/with_isolated_stderr.rb +0 -2
  33. data/lib/rspec/support/spec.rb +46 -26
  34. data/lib/rspec/support/version.rb +1 -1
  35. data/lib/rspec/support/warnings.rb +6 -6
  36. data/lib/rspec/support/with_keywords_when_needed.rb +33 -0
  37. data/lib/rspec/support.rb +87 -3
  38. data.tar.gz.sig +0 -0
  39. metadata +70 -52
  40. metadata.gz.sig +0 -0
  41. data/lib/rspec/support/version_checker.rb +0 -53
@@ -1,15 +1,17 @@
1
1
  RSpec::Support.require_rspec_support 'encoded_string'
2
2
  RSpec::Support.require_rspec_support 'hunk_generator'
3
+ RSpec::Support.require_rspec_support "object_formatter"
3
4
 
4
5
  require 'pp'
5
6
 
6
7
  module RSpec
7
8
  module Support
9
+ # rubocop:disable Metrics/ClassLength
8
10
  class Differ
9
11
  def diff(actual, expected)
10
12
  diff = ""
11
13
 
12
- if actual && expected
14
+ unless actual.nil? || expected.nil?
13
15
  if all_strings?(actual, expected)
14
16
  if any_multiline_strings?(actual, expected)
15
17
  diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected))
@@ -22,34 +24,35 @@ module RSpec
22
24
  diff.to_s
23
25
  end
24
26
 
27
+ # rubocop:disable Metrics/MethodLength
25
28
  def diff_as_string(actual, expected)
26
- @encoding = pick_encoding actual, expected
29
+ encoding = EncodedString.pick_encoding(actual, expected)
27
30
 
28
- @actual = EncodedString.new(actual, @encoding)
29
- @expected = EncodedString.new(expected, @encoding)
31
+ actual = EncodedString.new(actual, encoding)
32
+ expected = EncodedString.new(expected, encoding)
30
33
 
31
- output = EncodedString.new("\n", @encoding)
34
+ output = EncodedString.new("\n", encoding)
35
+ hunks = build_hunks(actual, expected)
32
36
 
33
37
  hunks.each_cons(2) do |prev_hunk, current_hunk|
34
38
  begin
35
39
  if current_hunk.overlaps?(prev_hunk)
36
40
  add_old_hunk_to_hunk(current_hunk, prev_hunk)
37
41
  else
38
- add_to_output(output, prev_hunk.diff(format).to_s)
42
+ add_to_output(output, prev_hunk.diff(format_type).to_s)
39
43
  end
40
44
  ensure
41
45
  add_to_output(output, "\n")
42
46
  end
43
47
  end
44
48
 
45
- if hunks.last
46
- finalize_output(output, hunks.last.diff(format).to_s)
47
- end
49
+ finalize_output(output, hunks.last.diff(format_type).to_s) if hunks.last
48
50
 
49
51
  color_diff output
50
52
  rescue Encoding::CompatibilityError
51
- handle_encoding_errors
53
+ handle_encoding_errors(actual, expected)
52
54
  end
55
+ # rubocop:enable Metrics/MethodLength
53
56
 
54
57
  def diff_as_object(actual, expected)
55
58
  actual_as_string = object_to_string(actual)
@@ -57,8 +60,9 @@ module RSpec
57
60
  diff_as_string(actual_as_string, expected_as_string)
58
61
  end
59
62
 
60
- attr_reader :color
61
- alias_method :color?, :color
63
+ def color?
64
+ @color
65
+ end
62
66
 
63
67
  def initialize(opts={})
64
68
  @color = opts.fetch(:color, false)
@@ -68,19 +72,19 @@ module RSpec
68
72
  private
69
73
 
70
74
  def no_procs?(*args)
71
- args.flatten.none? { |a| Proc === a}
75
+ safely_flatten(args).none? { |a| Proc === a }
72
76
  end
73
77
 
74
78
  def all_strings?(*args)
75
- args.flatten.all? { |a| String === a}
79
+ safely_flatten(args).all? { |a| String === a }
76
80
  end
77
81
 
78
82
  def any_multiline_strings?(*args)
79
- all_strings?(*args) && args.flatten.any? { |a| multiline?(a) }
83
+ all_strings?(*args) && safely_flatten(args).any? { |a| multiline?(a) }
80
84
  end
81
85
 
82
86
  def no_numbers?(*args)
83
- args.flatten.none? { |a| Numeric === a}
87
+ safely_flatten(args).none? { |a| Numeric === a }
84
88
  end
85
89
 
86
90
  def coerce_to_string(string_or_array)
@@ -93,7 +97,7 @@ module RSpec
93
97
  if Array === entry
94
98
  entry.inspect
95
99
  else
96
- entry.to_s.gsub("\n", "\\n")
100
+ entry.to_s.gsub("\n", "\\n").gsub("\r", "\\r")
97
101
  end
98
102
  end
99
103
  end
@@ -108,8 +112,8 @@ module RSpec
108
112
  end
109
113
  end
110
114
 
111
- def hunks
112
- @hunks ||= HunkGenerator.new(@actual, @expected).hunks
115
+ def build_hunks(actual, expected)
116
+ HunkGenerator.new(actual, expected).hunks
113
117
  end
114
118
 
115
119
  def finalize_output(output, final_line)
@@ -125,7 +129,12 @@ module RSpec
125
129
  hunk.merge(oldhunk)
126
130
  end
127
131
 
128
- def format
132
+ def safely_flatten(array)
133
+ array = array.flatten(1) until (array == array.flatten(1))
134
+ array
135
+ end
136
+
137
+ def format_type
129
138
  :unified
130
139
  end
131
140
 
@@ -152,7 +161,7 @@ module RSpec
152
161
  def color_diff(diff)
153
162
  return diff unless color?
154
163
 
155
- diff.lines.map { |line|
164
+ diff.lines.map do |line|
156
165
  case line[0].chr
157
166
  when "+"
158
167
  green line
@@ -163,43 +172,44 @@ module RSpec
163
172
  else
164
173
  normal(line)
165
174
  end
166
- }.join
175
+ end.join
167
176
  end
168
177
 
169
178
  def object_to_string(object)
170
179
  object = @object_preparer.call(object)
171
180
  case object
172
181
  when Hash
173
- object.keys.sort_by { |k| k.to_s }.map do |key|
174
- pp_key = PP.singleline_pp(key, "")
175
- pp_value = PP.singleline_pp(object[key], "")
176
-
177
- "#{pp_key} => #{pp_value},"
178
- end.join("\n")
182
+ hash_to_string(object)
183
+ when Array
184
+ PP.pp(ObjectFormatter.prepare_for_inspection(object), "".dup)
179
185
  when String
180
186
  object =~ /\n/ ? object : object.inspect
181
187
  else
182
- PP.pp(object,"")
188
+ PP.pp(object, "".dup)
183
189
  end
184
190
  end
185
191
 
186
- if String.method_defined?(:encoding)
187
- def pick_encoding(source_a, source_b)
188
- Encoding.compatible?(source_a, source_b) || Encoding.default_external
189
- end
190
- else
191
- def pick_encoding(source_a, source_b)
192
- end
192
+ def hash_to_string(hash)
193
+ formatted_hash = ObjectFormatter.prepare_for_inspection(hash)
194
+ formatted_hash.keys.sort_by { |k| k.to_s }.map do |key|
195
+ pp_key = PP.singleline_pp(key, "".dup)
196
+ pp_value = PP.singleline_pp(formatted_hash[key], "".dup)
197
+
198
+ "#{pp_key} => #{pp_value},"
199
+ end.join("\n")
193
200
  end
194
201
 
195
- def handle_encoding_errors
196
- if @actual.source_encoding != @expected.source_encoding
197
- "Could not produce a diff because the encoding of the actual string (#{@actual.source_encoding}) "+
198
- "differs from the encoding of the expected string (#{@expected.source_encoding})"
202
+ def handle_encoding_errors(actual, expected)
203
+ if actual.source_encoding != expected.source_encoding
204
+ "Could not produce a diff because the encoding of the actual string " \
205
+ "(#{actual.source_encoding}) differs from the encoding of the expected " \
206
+ "string (#{expected.source_encoding})"
199
207
  else
200
- "Could not produce a diff because of the encoding of the string (#{@expected.source_encoding})"
208
+ "Could not produce a diff because of the encoding of the string " \
209
+ "(#{expected.source_encoding})"
201
210
  end
202
211
  end
203
212
  end
213
+ # rubocop:enable Metrics/ClassLength
204
214
  end
205
215
  end
@@ -0,0 +1,63 @@
1
+ RSpec::Support.require_rspec_support 'ruby_features'
2
+
3
+ module RSpec
4
+ module Support
5
+ # @api private
6
+ #
7
+ # Replacement for fileutils#mkdir_p because we don't want to require parts
8
+ # of stdlib in RSpec.
9
+ class DirectoryMaker
10
+ # @api private
11
+ #
12
+ # Implements nested directory construction
13
+ def self.mkdir_p(path)
14
+ stack = generate_stack(path)
15
+ path.split(File::SEPARATOR).each do |part|
16
+ stack = generate_path(stack, part)
17
+ begin
18
+ Dir.mkdir(stack) unless directory_exists?(stack)
19
+ rescue Errno::EEXIST => e
20
+ raise e unless directory_exists?(stack)
21
+ rescue Errno::ENOTDIR => e
22
+ raise Errno::EEXIST, e.message
23
+ end
24
+ end
25
+ end
26
+
27
+ if OS.windows_file_path?
28
+ def self.generate_stack(path)
29
+ if path.start_with?(File::SEPARATOR)
30
+ File::SEPARATOR
31
+ elsif path[1] == ':'
32
+ ''
33
+ else
34
+ '.'
35
+ end
36
+ end
37
+ def self.generate_path(stack, part)
38
+ if stack == ''
39
+ part
40
+ elsif stack == File::SEPARATOR
41
+ File.join('', part)
42
+ else
43
+ File.join(stack, part)
44
+ end
45
+ end
46
+ else
47
+ def self.generate_stack(path)
48
+ path.start_with?(File::SEPARATOR) ? File::SEPARATOR : "."
49
+ end
50
+ def self.generate_path(stack, part)
51
+ File.join(stack, part)
52
+ end
53
+ end
54
+
55
+ def self.directory_exists?(dirname)
56
+ File.exist?(dirname) && File.directory?(dirname)
57
+ end
58
+ private_class_method :directory_exists?
59
+ private_class_method :generate_stack
60
+ private_class_method :generate_path
61
+ end
62
+ end
63
+ end
@@ -2,10 +2,16 @@ module RSpec
2
2
  module Support
3
3
  # @private
4
4
  class EncodedString
5
+ # Reduce allocations by storing constants.
6
+ UTF_8 = "UTF-8"
7
+ US_ASCII = "US-ASCII"
5
8
 
6
- MRI_UNICODE_UNKOWN_CHARACTER = "\xEF\xBF\xBD"
9
+ # Ruby's default replacement string is:
10
+ # U+FFFD ("\xEF\xBF\xBD"), for Unicode encoding forms, else
11
+ # ? ("\x3F")
12
+ REPLACE = "?"
7
13
 
8
- def initialize(string, encoding = nil)
14
+ def initialize(string, encoding=nil)
9
15
  @encoding = encoding
10
16
  @source_encoding = detect_source_encoding(string)
11
17
  @string = matching_encoding(string)
@@ -21,8 +27,18 @@ module RSpec
21
27
  @string << matching_encoding(string)
22
28
  end
23
29
 
24
- def split(regex_or_string)
25
- @string.split(matching_encoding(regex_or_string))
30
+ if Ruby.jruby?
31
+ def split(regex_or_string)
32
+ @string.split(matching_encoding(regex_or_string))
33
+ rescue ArgumentError
34
+ # JRuby raises an ArgumentError when splitting a source string that
35
+ # contains invalid bytes.
36
+ remove_invalid_bytes(@string).split regex_or_string
37
+ end
38
+ else
39
+ def split(regex_or_string)
40
+ @string.split(matching_encoding(regex_or_string))
41
+ end
26
42
  end
27
43
 
28
44
  def to_s
@@ -30,35 +46,114 @@ module RSpec
30
46
  end
31
47
  alias :to_str :to_s
32
48
 
33
- private
34
-
35
49
  if String.method_defined?(:encoding)
50
+
51
+ private
52
+
53
+ # Encoding Exceptions:
54
+ #
55
+ # Raised by Encoding and String methods:
56
+ # Encoding::UndefinedConversionError:
57
+ # when a transcoding operation fails
58
+ # if the String contains characters invalid for the target encoding
59
+ # e.g. "\x80".encode('UTF-8','ASCII-8BIT')
60
+ # vs "\x80".encode('UTF-8','ASCII-8BIT', undef: :replace, replace: '<undef>')
61
+ # # => '<undef>'
62
+ # Encoding::CompatibilityError
63
+ # when Encoding.compatible?(str1, str2) is nil
64
+ # e.g. utf_16le_emoji_string.split("\n")
65
+ # e.g. valid_unicode_string.encode(utf8_encoding) << ascii_string
66
+ # Encoding::InvalidByteSequenceError:
67
+ # when the string being transcoded contains a byte invalid for
68
+ # either the source or target encoding
69
+ # e.g. "\x80".encode('UTF-8','US-ASCII')
70
+ # vs "\x80".encode('UTF-8','US-ASCII', invalid: :replace, replace: '<byte>')
71
+ # # => '<byte>'
72
+ # ArgumentError
73
+ # when operating on a string with invalid bytes
74
+ # e.g."\x80".split("\n")
75
+ # TypeError
76
+ # when a symbol is passed as an encoding
77
+ # Encoding.find(:"UTF-8")
78
+ # when calling force_encoding on an object
79
+ # that doesn't respond to #to_str
80
+ #
81
+ # Raised by transcoding methods:
82
+ # Encoding::ConverterNotFoundError:
83
+ # when a named encoding does not correspond with a known converter
84
+ # e.g. 'abc'.force_encoding('UTF-8').encode('foo')
85
+ # or a converter path cannot be found
86
+ # e.g. "\x80".force_encoding('ASCII-8BIT').encode('Emacs-Mule')
87
+ #
88
+ # Raised by byte <-> char conversions
89
+ # RangeError: out of char range
90
+ # e.g. the UTF-16LE emoji: 128169.chr
36
91
  def matching_encoding(string)
92
+ string = remove_invalid_bytes(string)
37
93
  string.encode(@encoding)
38
94
  rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
39
- normalize_missing(string.encode(@encoding, :invalid => :replace, :undef => :replace))
95
+ # Originally defined as a constant to avoid unneeded allocations, this hash must
96
+ # be defined inline (without {}) to avoid warnings on Ruby 2.7
97
+ #
98
+ # In MRI 2.1 'invalid: :replace' changed to also replace an invalid byte sequence
99
+ # see https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L176
100
+ # https://www.ruby-forum.com/topic/6861247
101
+ # https://twitter.com/nalsh/status/553413844685438976
102
+ #
103
+ # For example, given:
104
+ # "\x80".force_encoding("Emacs-Mule").encode(:invalid => :replace).bytes.to_a
105
+ #
106
+ # On MRI 2.1 or above: 63 # '?'
107
+ # else : 128 # "\x80"
108
+ #
109
+ string.encode(@encoding, :invalid => :replace, :undef => :replace, :replace => REPLACE)
40
110
  rescue Encoding::ConverterNotFoundError
41
- normalize_missing(string.force_encoding(@encoding).encode(:invalid => :replace))
111
+ # Originally defined as a constant to avoid unneeded allocations, this hash must
112
+ # be defined inline (without {}) to avoid warnings on Ruby 2.7
113
+ string.dup.force_encoding(@encoding).encode(:invalid => :replace, :replace => REPLACE)
42
114
  end
43
115
 
44
- def normalize_missing(string)
45
- if @encoding.to_s == "UTF-8"
46
- string.gsub(MRI_UNICODE_UNKOWN_CHARACTER.force_encoding(@encoding), "?")
47
- else
48
- string
116
+ # Prevents raising ArgumentError
117
+ if String.method_defined?(:scrub)
118
+ # https://github.com/ruby/ruby/blob/eeb05e8c11/doc/NEWS-2.1.0#L120-L123
119
+ # https://github.com/ruby/ruby/blob/v2_1_0/string.c#L8242
120
+ # https://github.com/hsbt/string-scrub
121
+ # https://github.com/rubinius/rubinius/blob/v2.5.2/kernel/common/string.rb#L1913-L1972
122
+ def remove_invalid_bytes(string)
123
+ string.scrub(REPLACE)
124
+ end
125
+ else
126
+ # http://stackoverflow.com/a/8711118/879854
127
+ # Loop over chars in a string replacing chars
128
+ # with invalid encoding, which is a pretty good proxy
129
+ # for the invalid byte sequence that causes an ArgumentError
130
+ def remove_invalid_bytes(string)
131
+ string.chars.map do |char|
132
+ char.valid_encoding? ? char : REPLACE
133
+ end.join
49
134
  end
50
135
  end
51
136
 
52
137
  def detect_source_encoding(string)
53
138
  string.encoding
54
139
  end
140
+
141
+ def self.pick_encoding(source_a, source_b)
142
+ Encoding.compatible?(source_a, source_b) || Encoding.default_external
143
+ end
55
144
  else
145
+
146
+ def self.pick_encoding(_source_a, _source_b)
147
+ end
148
+
149
+ private
150
+
56
151
  def matching_encoding(string)
57
152
  string
58
153
  end
59
154
 
60
- def detect_source_encoding(string)
61
- 'US-ASCII'
155
+ def detect_source_encoding(_string)
156
+ US_ASCII
62
157
  end
63
158
  end
64
159
  end
@@ -6,14 +6,14 @@ module RSpec
6
6
  module FuzzyMatcher
7
7
  # @api private
8
8
  def self.values_match?(expected, actual)
9
- if Array === expected && Enumerable === actual
9
+ if Hash === actual
10
+ return hashes_match?(expected, actual) if Hash === expected
11
+ elsif Array === expected && Enumerable === actual && !(Struct === actual)
10
12
  return arrays_match?(expected, actual.to_a)
11
- elsif Hash === expected && Hash === actual
12
- return hashes_match?(expected, actual)
13
- elsif actual == expected
14
- return true
15
13
  end
16
14
 
15
+ return true if expected == actual
16
+
17
17
  begin
18
18
  expected === actual
19
19
  rescue ArgumentError
@@ -46,4 +46,3 @@ module RSpec
46
46
  end
47
47
  end
48
48
  end
49
-
@@ -42,7 +42,6 @@ module RSpec
42
42
  def context_lines
43
43
  3
44
44
  end
45
-
46
45
  end
47
46
  end
48
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