sspec-support 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Changelog.md +242 -0
- data/LICENSE.md +23 -0
- data/README.md +40 -0
- data/lib/rspec/support.rb +149 -0
- data/lib/rspec/support/caller_filter.rb +83 -0
- data/lib/rspec/support/comparable_version.rb +46 -0
- data/lib/rspec/support/differ.rb +215 -0
- data/lib/rspec/support/directory_maker.rb +63 -0
- data/lib/rspec/support/encoded_string.rb +165 -0
- data/lib/rspec/support/fuzzy_matcher.rb +48 -0
- data/lib/rspec/support/hunk_generator.rb +47 -0
- data/lib/rspec/support/matcher_definition.rb +42 -0
- data/lib/rspec/support/method_signature_verifier.rb +426 -0
- data/lib/rspec/support/mutex.rb +73 -0
- data/lib/rspec/support/object_formatter.rb +275 -0
- data/lib/rspec/support/recursive_const_methods.rb +76 -0
- data/lib/rspec/support/reentrant_mutex.rb +53 -0
- data/lib/rspec/support/ruby_features.rb +176 -0
- data/lib/rspec/support/source.rb +75 -0
- data/lib/rspec/support/source/location.rb +21 -0
- data/lib/rspec/support/source/node.rb +110 -0
- data/lib/rspec/support/source/token.rb +87 -0
- data/lib/rspec/support/spec.rb +81 -0
- data/lib/rspec/support/spec/deprecation_helpers.rb +64 -0
- data/lib/rspec/support/spec/formatting_support.rb +9 -0
- data/lib/rspec/support/spec/in_sub_process.rb +69 -0
- data/lib/rspec/support/spec/library_wide_checks.rb +150 -0
- data/lib/rspec/support/spec/shell_out.rb +84 -0
- data/lib/rspec/support/spec/stderr_splitter.rb +63 -0
- data/lib/rspec/support/spec/string_matcher.rb +46 -0
- data/lib/rspec/support/spec/with_isolated_directory.rb +13 -0
- data/lib/rspec/support/spec/with_isolated_stderr.rb +13 -0
- data/lib/rspec/support/version.rb +7 -0
- data/lib/rspec/support/warnings.rb +39 -0
- 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
|