sspec-support 3.8.0

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