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.
- 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
|