rspec-support 3.0.4 → 3.12.1

Sign up to get free protection for your applications and to get access to all the features.
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