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,83 @@
1
+ RSpec::Support.require_rspec_support "ruby_features"
2
+
3
+ module RSpec
4
+ # Consistent implementation for "cleaning" the caller method to strip out
5
+ # non-rspec lines. This enables errors to be reported at the call site in
6
+ # the code using the library, which is far more useful than the particular
7
+ # internal method that raised an error.
8
+ class CallerFilter
9
+ RSPEC_LIBS = %w[
10
+ core
11
+ mocks
12
+ expectations
13
+ support
14
+ matchers
15
+ rails
16
+ ]
17
+
18
+ ADDITIONAL_TOP_LEVEL_FILES = %w[ autorun ]
19
+
20
+ LIB_REGEX = %r{/lib/rspec/(#{(RSPEC_LIBS + ADDITIONAL_TOP_LEVEL_FILES).join('|')})(\.rb|/)}
21
+
22
+ # rubygems/core_ext/kernel_require.rb isn't actually part of rspec (obviously) but we want
23
+ # it ignored when we are looking for the first meaningful line of the backtrace outside
24
+ # of RSpec. It can show up in the backtrace as the immediate first caller
25
+ # when `CallerFilter.first_non_rspec_line` is called from the top level of a required
26
+ # file, but it depends on if rubygems is loaded or not. We don't want to have to deal
27
+ # with this complexity in our `RSpec.deprecate` calls, so we ignore it here.
28
+ IGNORE_REGEX = Regexp.union(LIB_REGEX, "rubygems/core_ext/kernel_require.rb")
29
+
30
+ if RSpec::Support::RubyFeatures.caller_locations_supported?
31
+ # This supports args because it's more efficient when the caller specifies
32
+ # these. It allows us to skip frames the caller knows are part of RSpec,
33
+ # and to decrease the increment size if the caller is confident the line will
34
+ # be found in a small number of stack frames from `skip_frames`.
35
+ #
36
+ # Note that there is a risk to passing a `skip_frames` value that is too high:
37
+ # If it skippped the first non-rspec line, then this method would return the
38
+ # 2nd or 3rd (or whatever) non-rspec line. Thus, you generally shouldn't pass
39
+ # values for these parameters, particularly since most places that use this are
40
+ # not hot spots (generally it gets used for deprecation warnings). However,
41
+ # if you do have a hot spot that calls this, passing `skip_frames` can make
42
+ # a significant difference. Just make sure that that particular use is tested
43
+ # so that if the provided `skip_frames` changes to no longer be accurate in
44
+ # such a way that would return the wrong stack frame, a test will fail to tell you.
45
+ #
46
+ # See benchmarks/skip_frames_for_caller_filter.rb for measurements.
47
+ def self.first_non_rspec_line(skip_frames=3, increment=5)
48
+ # Why a default `skip_frames` of 3?
49
+ # By the time `caller_locations` is called below, the first 3 frames are:
50
+ # lib/rspec/support/caller_filter.rb:63:in `block in first_non_rspec_line'
51
+ # lib/rspec/support/caller_filter.rb:62:in `loop'
52
+ # lib/rspec/support/caller_filter.rb:62:in `first_non_rspec_line'
53
+
54
+ # `caller` is an expensive method that scales linearly with the size of
55
+ # the stack. The performance hit for fetching it in chunks is small,
56
+ # and since the target line is probably near the top of the stack, the
57
+ # overall improvement of a chunked search like this is significant.
58
+ #
59
+ # See benchmarks/caller.rb for measurements.
60
+
61
+ # The default increment of 5 for this method are mostly arbitrary, but
62
+ # is chosen to give good performance on the common case of creating a double.
63
+
64
+ loop do
65
+ stack = caller_locations(skip_frames, increment)
66
+ raise "No non-lib lines in stack" unless stack
67
+
68
+ line = stack.find { |l| l.path !~ IGNORE_REGEX }
69
+ return line.to_s if line
70
+
71
+ skip_frames += increment
72
+ increment *= 2 # The choice of two here is arbitrary.
73
+ end
74
+ end
75
+ else
76
+ # Earlier rubies do not support the two argument form of `caller`. This
77
+ # fallback is logically the same, but slower.
78
+ def self.first_non_rspec_line(*)
79
+ caller.find { |line| line !~ IGNORE_REGEX }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ module RSpec
2
+ module Support
3
+ # @private
4
+ class ComparableVersion
5
+ include Comparable
6
+
7
+ attr_reader :string
8
+
9
+ def initialize(string)
10
+ @string = string
11
+ end
12
+
13
+ def <=>(other)
14
+ other = self.class.new(other) unless other.is_a?(self.class)
15
+
16
+ return 0 if string == other.string
17
+
18
+ longer_segment_count = [self, other].map { |version| version.segments.count }.max
19
+
20
+ longer_segment_count.times do |index|
21
+ self_segment = segments[index] || 0
22
+ other_segment = other.segments[index] || 0
23
+
24
+ if self_segment.class == other_segment.class
25
+ result = self_segment <=> other_segment
26
+ return result unless result == 0
27
+ else
28
+ return self_segment.is_a?(String) ? -1 : 1
29
+ end
30
+ end
31
+
32
+ 0
33
+ end
34
+
35
+ def segments
36
+ @segments ||= string.scan(/[a-z]+|\d+/i).map do |segment|
37
+ if segment =~ /\A\d+\z/
38
+ segment.to_i
39
+ else
40
+ segment
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,215 @@
1
+ RSpec::Support.require_rspec_support 'encoded_string'
2
+ RSpec::Support.require_rspec_support 'hunk_generator'
3
+ RSpec::Support.require_rspec_support "object_formatter"
4
+
5
+ require 'pp'
6
+
7
+ module RSpec
8
+ module Support
9
+ # rubocop:disable ClassLength
10
+ class Differ
11
+ def diff(actual, expected)
12
+ diff = ""
13
+
14
+ if actual && expected
15
+ if all_strings?(actual, expected)
16
+ if any_multiline_strings?(actual, expected)
17
+ diff = diff_as_string(coerce_to_string(actual), coerce_to_string(expected))
18
+ end
19
+ elsif no_procs?(actual, expected) && no_numbers?(actual, expected)
20
+ diff = diff_as_object(actual, expected)
21
+ end
22
+ end
23
+
24
+ diff.to_s
25
+ end
26
+
27
+ # rubocop:disable MethodLength
28
+ def diff_as_string(actual, expected)
29
+ encoding = EncodedString.pick_encoding(actual, expected)
30
+
31
+ actual = EncodedString.new(actual, encoding)
32
+ expected = EncodedString.new(expected, encoding)
33
+
34
+ output = EncodedString.new("\n", encoding)
35
+ hunks = build_hunks(actual, expected)
36
+
37
+ hunks.each_cons(2) do |prev_hunk, current_hunk|
38
+ begin
39
+ if current_hunk.overlaps?(prev_hunk)
40
+ add_old_hunk_to_hunk(current_hunk, prev_hunk)
41
+ else
42
+ add_to_output(output, prev_hunk.diff(format_type).to_s)
43
+ end
44
+ ensure
45
+ add_to_output(output, "\n")
46
+ end
47
+ end
48
+
49
+ finalize_output(output, hunks.last.diff(format_type).to_s) if hunks.last
50
+
51
+ color_diff output
52
+ rescue Encoding::CompatibilityError
53
+ handle_encoding_errors(actual, expected)
54
+ end
55
+ # rubocop:enable MethodLength
56
+
57
+ def diff_as_object(actual, expected)
58
+ actual_as_string = object_to_string(actual)
59
+ expected_as_string = object_to_string(expected)
60
+ diff_as_string(actual_as_string, expected_as_string)
61
+ end
62
+
63
+ def color?
64
+ @color
65
+ end
66
+
67
+ def initialize(opts={})
68
+ @color = opts.fetch(:color, false)
69
+ @object_preparer = opts.fetch(:object_preparer, lambda { |string| string })
70
+ end
71
+
72
+ private
73
+
74
+ def no_procs?(*args)
75
+ safely_flatten(args).none? { |a| Proc === a }
76
+ end
77
+
78
+ def all_strings?(*args)
79
+ safely_flatten(args).all? { |a| String === a }
80
+ end
81
+
82
+ def any_multiline_strings?(*args)
83
+ all_strings?(*args) && safely_flatten(args).any? { |a| multiline?(a) }
84
+ end
85
+
86
+ def no_numbers?(*args)
87
+ safely_flatten(args).none? { |a| Numeric === a }
88
+ end
89
+
90
+ def coerce_to_string(string_or_array)
91
+ return string_or_array unless Array === string_or_array
92
+ diffably_stringify(string_or_array).join("\n")
93
+ end
94
+
95
+ def diffably_stringify(array)
96
+ array.map do |entry|
97
+ if Array === entry
98
+ entry.inspect
99
+ else
100
+ entry.to_s.gsub("\n", "\\n")
101
+ end
102
+ end
103
+ end
104
+
105
+ if String.method_defined?(:encoding)
106
+ def multiline?(string)
107
+ string.include?("\n".encode(string.encoding))
108
+ end
109
+ else
110
+ def multiline?(string)
111
+ string.include?("\n")
112
+ end
113
+ end
114
+
115
+ def build_hunks(actual, expected)
116
+ HunkGenerator.new(actual, expected).hunks
117
+ end
118
+
119
+ def finalize_output(output, final_line)
120
+ add_to_output(output, final_line)
121
+ add_to_output(output, "\n")
122
+ end
123
+
124
+ def add_to_output(output, string)
125
+ output << string
126
+ end
127
+
128
+ def add_old_hunk_to_hunk(hunk, oldhunk)
129
+ hunk.merge(oldhunk)
130
+ end
131
+
132
+ def safely_flatten(array)
133
+ array = array.flatten(1) until (array == array.flatten(1))
134
+ array
135
+ end
136
+
137
+ def format_type
138
+ :unified
139
+ end
140
+
141
+ def color(text, color_code)
142
+ "\e[#{color_code}m#{text}\e[0m"
143
+ end
144
+
145
+ def red(text)
146
+ color(text, 31)
147
+ end
148
+
149
+ def green(text)
150
+ color(text, 32)
151
+ end
152
+
153
+ def blue(text)
154
+ color(text, 34)
155
+ end
156
+
157
+ def normal(text)
158
+ color(text, 0)
159
+ end
160
+
161
+ def color_diff(diff)
162
+ return diff unless color?
163
+
164
+ diff.lines.map do |line|
165
+ case line[0].chr
166
+ when "+"
167
+ green line
168
+ when "-"
169
+ red line
170
+ when "@"
171
+ line[1].chr == "@" ? blue(line) : normal(line)
172
+ else
173
+ normal(line)
174
+ end
175
+ end.join
176
+ end
177
+
178
+ def object_to_string(object)
179
+ object = @object_preparer.call(object)
180
+ case object
181
+ when Hash
182
+ hash_to_string(object)
183
+ when Array
184
+ PP.pp(ObjectFormatter.prepare_for_inspection(object), "".dup)
185
+ when String
186
+ object =~ /\n/ ? object : object.inspect
187
+ else
188
+ PP.pp(object, "".dup)
189
+ end
190
+ end
191
+
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")
200
+ end
201
+
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})"
207
+ else
208
+ "Could not produce a diff because of the encoding of the string " \
209
+ "(#{expected.source_encoding})"
210
+ end
211
+ end
212
+ end
213
+ # rubocop:enable ClassLength
214
+ end
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
@@ -0,0 +1,165 @@
1
+ module RSpec
2
+ module Support
3
+ # @private
4
+ class EncodedString
5
+ # Reduce allocations by storing constants.
6
+ UTF_8 = "UTF-8"
7
+ US_ASCII = "US-ASCII"
8
+ #
9
+ # In MRI 2.1 'invalid: :replace' changed to also replace an invalid byte sequence
10
+ # see https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L176
11
+ # https://www.ruby-forum.com/topic/6861247
12
+ # https://twitter.com/nalsh/status/553413844685438976
13
+ #
14
+ # For example, given:
15
+ # "\x80".force_encoding("Emacs-Mule").encode(:invalid => :replace).bytes.to_a
16
+ #
17
+ # On MRI 2.1 or above: 63 # '?'
18
+ # else : 128 # "\x80"
19
+ #
20
+ # Ruby's default replacement string is:
21
+ # U+FFFD ("\xEF\xBF\xBD"), for Unicode encoding forms, else
22
+ # ? ("\x3F")
23
+ REPLACE = "?"
24
+ ENCODE_UNCONVERTABLE_BYTES = {
25
+ :invalid => :replace,
26
+ :undef => :replace,
27
+ :replace => REPLACE
28
+ }
29
+ ENCODE_NO_CONVERTER = {
30
+ :invalid => :replace,
31
+ :replace => REPLACE
32
+ }
33
+
34
+ def initialize(string, encoding=nil)
35
+ @encoding = encoding
36
+ @source_encoding = detect_source_encoding(string)
37
+ @string = matching_encoding(string)
38
+ end
39
+ attr_reader :source_encoding
40
+
41
+ delegated_methods = String.instance_methods.map(&:to_s) & %w[eql? lines == encoding empty?]
42
+ delegated_methods.each do |name|
43
+ define_method(name) { |*args, &block| @string.__send__(name, *args, &block) }
44
+ end
45
+
46
+ def <<(string)
47
+ @string << matching_encoding(string)
48
+ end
49
+
50
+ if Ruby.jruby?
51
+ def split(regex_or_string)
52
+ @string.split(matching_encoding(regex_or_string))
53
+ rescue ArgumentError
54
+ # JRuby raises an ArgumentError when splitting a source string that
55
+ # contains invalid bytes.
56
+ remove_invalid_bytes(@string).split regex_or_string
57
+ end
58
+ else
59
+ def split(regex_or_string)
60
+ @string.split(matching_encoding(regex_or_string))
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ @string
66
+ end
67
+ alias :to_str :to_s
68
+
69
+ if String.method_defined?(:encoding)
70
+
71
+ private
72
+
73
+ # Encoding Exceptions:
74
+ #
75
+ # Raised by Encoding and String methods:
76
+ # Encoding::UndefinedConversionError:
77
+ # when a transcoding operation fails
78
+ # if the String contains characters invalid for the target encoding
79
+ # e.g. "\x80".encode('UTF-8','ASCII-8BIT')
80
+ # vs "\x80".encode('UTF-8','ASCII-8BIT', undef: :replace, replace: '<undef>')
81
+ # # => '<undef>'
82
+ # Encoding::CompatibilityError
83
+ # when Encoding.compatibile?(str1, str2) is nil
84
+ # e.g. utf_16le_emoji_string.split("\n")
85
+ # e.g. valid_unicode_string.encode(utf8_encoding) << ascii_string
86
+ # Encoding::InvalidByteSequenceError:
87
+ # when the string being transcoded contains a byte invalid for
88
+ # either the source or target encoding
89
+ # e.g. "\x80".encode('UTF-8','US-ASCII')
90
+ # vs "\x80".encode('UTF-8','US-ASCII', invalid: :replace, replace: '<byte>')
91
+ # # => '<byte>'
92
+ # ArgumentError
93
+ # when operating on a string with invalid bytes
94
+ # e.g."\x80".split("\n")
95
+ # TypeError
96
+ # when a symbol is passed as an encoding
97
+ # Encoding.find(:"UTF-8")
98
+ # when calling force_encoding on an object
99
+ # that doesn't respond to #to_str
100
+ #
101
+ # Raised by transcoding methods:
102
+ # Encoding::ConverterNotFoundError:
103
+ # when a named encoding does not correspond with a known converter
104
+ # e.g. 'abc'.force_encoding('UTF-8').encode('foo')
105
+ # or a converter path cannot be found
106
+ # e.g. "\x80".force_encoding('ASCII-8BIT').encode('Emacs-Mule')
107
+ #
108
+ # Raised by byte <-> char conversions
109
+ # RangeError: out of char range
110
+ # e.g. the UTF-16LE emoji: 128169.chr
111
+ def matching_encoding(string)
112
+ string = remove_invalid_bytes(string)
113
+ string.encode(@encoding)
114
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
115
+ string.encode(@encoding, ENCODE_UNCONVERTABLE_BYTES)
116
+ rescue Encoding::ConverterNotFoundError
117
+ string.dup.force_encoding(@encoding).encode(ENCODE_NO_CONVERTER)
118
+ end
119
+
120
+ # Prevents raising ArgumentError
121
+ if String.method_defined?(:scrub)
122
+ # https://github.com/ruby/ruby/blob/eeb05e8c11/doc/NEWS-2.1.0#L120-L123
123
+ # https://github.com/ruby/ruby/blob/v2_1_0/string.c#L8242
124
+ # https://github.com/hsbt/string-scrub
125
+ # https://github.com/rubinius/rubinius/blob/v2.5.2/kernel/common/string.rb#L1913-L1972
126
+ def remove_invalid_bytes(string)
127
+ string.scrub(REPLACE)
128
+ end
129
+ else
130
+ # http://stackoverflow.com/a/8711118/879854
131
+ # Loop over chars in a string replacing chars
132
+ # with invalid encoding, which is a pretty good proxy
133
+ # for the invalid byte sequence that causes an ArgumentError
134
+ def remove_invalid_bytes(string)
135
+ string.chars.map do |char|
136
+ char.valid_encoding? ? char : REPLACE
137
+ end.join
138
+ end
139
+ end
140
+
141
+ def detect_source_encoding(string)
142
+ string.encoding
143
+ end
144
+
145
+ def self.pick_encoding(source_a, source_b)
146
+ Encoding.compatible?(source_a, source_b) || Encoding.default_external
147
+ end
148
+ else
149
+
150
+ def self.pick_encoding(_source_a, _source_b)
151
+ end
152
+
153
+ private
154
+
155
+ def matching_encoding(string)
156
+ string
157
+ end
158
+
159
+ def detect_source_encoding(_string)
160
+ US_ASCII
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end