json 2.3.1 → 2.5.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +22 -0
  3. data/LICENSE +56 -0
  4. data/VERSION +1 -1
  5. data/ext/json/ext/generator/generator.c +60 -11
  6. data/ext/json/ext/generator/generator.h +5 -2
  7. data/ext/json/ext/parser/extconf.rb +25 -0
  8. data/ext/json/ext/parser/parser.c +110 -68
  9. data/ext/json/ext/parser/parser.h +1 -0
  10. data/ext/json/ext/parser/parser.rl +67 -25
  11. data/ext/json/extconf.rb +1 -0
  12. data/json.gemspec +11 -77
  13. data/lib/json.rb +171 -0
  14. data/lib/json/add/complex.rb +0 -1
  15. data/lib/json/add/rational.rb +0 -1
  16. data/lib/json/common.rb +240 -228
  17. data/lib/json/pure/generator.rb +28 -8
  18. data/lib/json/pure/parser.rb +20 -2
  19. data/lib/json/version.rb +1 -1
  20. data/tests/fixtures/fail29.json +1 -0
  21. data/tests/fixtures/fail30.json +1 -0
  22. data/tests/fixtures/fail31.json +1 -0
  23. data/tests/fixtures/fail32.json +1 -0
  24. data/tests/json_addition_test.rb +0 -4
  25. data/tests/json_common_interface_test.rb +43 -0
  26. data/tests/json_fixtures_test.rb +3 -0
  27. data/tests/json_generator_test.rb +16 -38
  28. data/tests/json_parser_test.rb +25 -0
  29. data/tests/lib/core_assertions.rb +763 -0
  30. data/tests/lib/envutil.rb +365 -0
  31. data/tests/lib/find_executable.rb +22 -0
  32. data/tests/lib/helper.rb +4 -0
  33. data/tests/ractor_test.rb +30 -0
  34. data/tests/test_helper.rb +3 -3
  35. metadata +16 -37
  36. data/.gitignore +0 -18
  37. data/.travis.yml +0 -26
  38. data/README-json-jruby.md +0 -33
  39. data/Rakefile +0 -334
  40. data/diagrams/.keep +0 -0
  41. data/install.rb +0 -23
  42. data/java/src/json/ext/ByteListTranscoder.java +0 -166
  43. data/java/src/json/ext/Generator.java +0 -466
  44. data/java/src/json/ext/GeneratorMethods.java +0 -231
  45. data/java/src/json/ext/GeneratorService.java +0 -42
  46. data/java/src/json/ext/GeneratorState.java +0 -490
  47. data/java/src/json/ext/OptionsReader.java +0 -113
  48. data/java/src/json/ext/Parser.java +0 -2362
  49. data/java/src/json/ext/Parser.rl +0 -893
  50. data/java/src/json/ext/ParserService.java +0 -34
  51. data/java/src/json/ext/RuntimeInfo.java +0 -116
  52. data/java/src/json/ext/StringDecoder.java +0 -166
  53. data/java/src/json/ext/StringEncoder.java +0 -111
  54. data/java/src/json/ext/Utils.java +0 -88
  55. data/json-java.gemspec +0 -37
  56. data/json_pure.gemspec +0 -33
  57. data/references/rfc7159.txt +0 -899
  58. data/tools/diff.sh +0 -18
  59. data/tools/fuzz.rb +0 -131
  60. data/tools/server.rb +0 -62
@@ -37,20 +37,26 @@ module JSON
37
37
  '\\' => '\\\\',
38
38
  } # :nodoc:
39
39
 
40
+ ESCAPE_SLASH_MAP = MAP.merge(
41
+ '/' => '\\/',
42
+ )
43
+
40
44
  # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
41
45
  # UTF16 big endian characters as \u????, and return it.
42
- def utf8_to_json(string) # :nodoc:
46
+ def utf8_to_json(string, escape_slash = false) # :nodoc:
43
47
  string = string.dup
44
48
  string.force_encoding(::Encoding::ASCII_8BIT)
45
- string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] }
49
+ map = escape_slash ? ESCAPE_SLASH_MAP : MAP
50
+ string.gsub!(/[\/"\\\x0-\x1f]/) { map[$&] || $& }
46
51
  string.force_encoding(::Encoding::UTF_8)
47
52
  string
48
53
  end
49
54
 
50
- def utf8_to_json_ascii(string) # :nodoc:
55
+ def utf8_to_json_ascii(string, escape_slash = false) # :nodoc:
51
56
  string = string.dup
52
57
  string.force_encoding(::Encoding::ASCII_8BIT)
53
- string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] }
58
+ map = escape_slash ? ESCAPE_SLASH_MAP : MAP
59
+ string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& }
54
60
  string.gsub!(/(
55
61
  (?:
56
62
  [\xc2-\xdf][\x80-\xbf] |
@@ -109,6 +115,7 @@ module JSON
109
115
  # * *space_before*: a string that is put before a : pair delimiter (default: ''),
110
116
  # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
111
117
  # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
118
+ # * *escape_slash*: true if forward slash (/) should be escaped (default: false)
112
119
  # * *check_circular*: is deprecated now, use the :max_nesting option instead,
113
120
  # * *max_nesting*: sets the maximum level of data structure nesting in
114
121
  # the generated JSON, max_nesting = 0 if no maximum should be checked.
@@ -123,6 +130,7 @@ module JSON
123
130
  @array_nl = ''
124
131
  @allow_nan = false
125
132
  @ascii_only = false
133
+ @escape_slash = false
126
134
  @buffer_initial_length = 1024
127
135
  configure opts
128
136
  end
@@ -148,6 +156,10 @@ module JSON
148
156
  # the generated JSON, max_nesting = 0 if no maximum is checked.
149
157
  attr_accessor :max_nesting
150
158
 
159
+ # If this attribute is set to true, forward slashes will be escaped in
160
+ # all json strings.
161
+ attr_accessor :escape_slash
162
+
151
163
  # :stopdoc:
152
164
  attr_reader :buffer_initial_length
153
165
 
@@ -187,6 +199,11 @@ module JSON
187
199
  @ascii_only
188
200
  end
189
201
 
202
+ # Returns true, if forward slashes are escaped. Otherwise returns false.
203
+ def escape_slash?
204
+ @escape_slash
205
+ end
206
+
190
207
  # Configure this State instance with the Hash _opts_, and return
191
208
  # itself.
192
209
  def configure(opts)
@@ -209,6 +226,7 @@ module JSON
209
226
  @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
210
227
  @depth = opts[:depth] || 0
211
228
  @buffer_initial_length ||= opts[:buffer_initial_length]
229
+ @escape_slash = !!opts[:escape_slash] if opts.key?(:escape_slash)
212
230
 
213
231
  if !opts.key?(:max_nesting) # defaults to 100
214
232
  @max_nesting = 100
@@ -314,8 +332,10 @@ module JSON
314
332
  first = false
315
333
  }
316
334
  depth = state.depth -= 1
317
- result << state.object_nl
318
- result << state.indent * depth if indent
335
+ unless first
336
+ result << state.object_nl
337
+ result << state.indent * depth if indent
338
+ end
319
339
  result << '}'
320
340
  result
321
341
  end
@@ -399,9 +419,9 @@ module JSON
399
419
  string = encode(::Encoding::UTF_8)
400
420
  end
401
421
  if state.ascii_only?
402
- '"' << JSON.utf8_to_json_ascii(string) << '"'
422
+ '"' << JSON.utf8_to_json_ascii(string, state.escape_slash) << '"'
403
423
  else
404
- '"' << JSON.utf8_to_json(string) << '"'
424
+ '"' << JSON.utf8_to_json(string, state.escape_slash) << '"'
405
425
  end
406
426
  end
407
427
 
@@ -61,6 +61,8 @@ module JSON
61
61
  # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
62
62
  # defiance of RFC 7159 to be parsed by the Parser. This option defaults
63
63
  # to false.
64
+ # * *freeze*: If set to true, all parsed objects will be frozen. Parsed
65
+ # string will be deduplicated if possible.
64
66
  # * *symbolize_names*: If set to true, returns symbols for the names
65
67
  # (keys) in a JSON object. Otherwise strings are returned, which is
66
68
  # also the default. It's not possible to use this option in
@@ -86,6 +88,7 @@ module JSON
86
88
  end
87
89
  @allow_nan = !!opts[:allow_nan]
88
90
  @symbolize_names = !!opts[:symbolize_names]
91
+ @freeze = !!opts[:freeze]
89
92
  if opts.key?(:create_additions)
90
93
  @create_additions = !!opts[:create_additions]
91
94
  else
@@ -120,6 +123,7 @@ module JSON
120
123
  obj = parse_value
121
124
  UNPARSED.equal?(obj) and raise ParserError,
122
125
  "source is not valid JSON!"
126
+ obj.freeze if @freeze
123
127
  end
124
128
  while !eos? && skip(IGNORE) do end
125
129
  eos? or raise ParserError, "source is not valid JSON!"
@@ -161,6 +165,7 @@ module JSON
161
165
  EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
162
166
  end
163
167
 
168
+ STR_UMINUS = ''.respond_to?(:-@)
164
169
  def parse_string
165
170
  if scan(STRING)
166
171
  return '' if self[1].empty?
@@ -180,6 +185,15 @@ module JSON
180
185
  if string.respond_to?(:force_encoding)
181
186
  string.force_encoding(::Encoding::UTF_8)
182
187
  end
188
+
189
+ if @freeze
190
+ if STR_UMINUS
191
+ string = -string
192
+ else
193
+ string.freeze
194
+ end
195
+ end
196
+
183
197
  if @create_additions and @match_string
184
198
  for (regexp, klass) in @match_string
185
199
  klass.json_creatable? or next
@@ -242,8 +256,10 @@ module JSON
242
256
  @max_nesting.nonzero? && @current_nesting > @max_nesting
243
257
  result = @array_class.new
244
258
  delim = false
245
- until eos?
259
+ loop do
246
260
  case
261
+ when eos?
262
+ raise ParserError, "unexpected end of string while parsing array"
247
263
  when !UNPARSED.equal?(value = parse_value)
248
264
  delim = false
249
265
  result << value
@@ -274,8 +290,10 @@ module JSON
274
290
  @max_nesting.nonzero? && @current_nesting > @max_nesting
275
291
  result = @object_class.new
276
292
  delim = false
277
- until eos?
293
+ loop do
278
294
  case
295
+ when eos?
296
+ raise ParserError, "unexpected end of string while parsing object"
279
297
  when !UNPARSED.equal?(string = parse_string)
280
298
  skip(IGNORE)
281
299
  unless scan(PAIR_DELIMITER)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
  module JSON
3
3
  # JSON version
4
- VERSION = '2.3.1'
4
+ VERSION = '2.5.1'
5
5
  VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
6
6
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
7
7
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -0,0 +1 @@
1
+ {
@@ -0,0 +1 @@
1
+ [
@@ -0,0 +1 @@
1
+ [1, 2, 3,
@@ -0,0 +1 @@
1
+ {"foo": "bar"
@@ -195,9 +195,5 @@ class JSONAdditionTest < Test::Unit::TestCase
195
195
  def test_set
196
196
  s = Set.new([:a, :b, :c, :a])
197
197
  assert_equal s, JSON.parse(JSON(s), :create_additions => true)
198
- ss = SortedSet.new([:d, :b, :a, :c])
199
- ss_again = JSON.parse(JSON(ss), :create_additions => true)
200
- assert_kind_of ss.class, ss_again
201
- assert_equal ss, ss_again
202
198
  end
203
199
  end
@@ -123,4 +123,47 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
123
123
  assert_equal @json, JSON(@hash)
124
124
  assert_equal @hash, JSON(@json)
125
125
  end
126
+
127
+ def test_load_file
128
+ test_load_shared(:load_file)
129
+ end
130
+
131
+ def test_load_file!
132
+ test_load_shared(:load_file!)
133
+ end
134
+
135
+ def test_load_file_with_option
136
+ test_load_file_with_option_shared(:load_file)
137
+ end
138
+
139
+ def test_load_file_with_option!
140
+ test_load_file_with_option_shared(:load_file!)
141
+ end
142
+
143
+ private
144
+
145
+ def test_load_shared(method_name)
146
+ temp_file_containing(@json) do |filespec|
147
+ assert_equal JSON.public_send(method_name, filespec), @hash
148
+ end
149
+ end
150
+
151
+ def test_load_file_with_option_shared(method_name)
152
+ temp_file_containing(@json) do |filespec|
153
+ parsed_object = JSON.public_send(method_name, filespec, symbolize_names: true)
154
+ key_classes = parsed_object.keys.map(&:class)
155
+ assert_include(key_classes, Symbol)
156
+ assert_not_include(key_classes, String)
157
+ end
158
+ end
159
+
160
+ def temp_file_containing(text, file_prefix = '')
161
+ raise "This method must be called with a code block." unless block_given?
162
+
163
+ Tempfile.create(file_prefix) do |file|
164
+ file << text
165
+ file.close
166
+ yield file.path
167
+ end
168
+ end
126
169
  end
@@ -10,6 +10,7 @@ class JSONFixturesTest < Test::Unit::TestCase
10
10
  end
11
11
 
12
12
  def test_passing
13
+ verbose_bak, $VERBOSE = $VERBOSE, nil
13
14
  for name, source in @passed
14
15
  begin
15
16
  assert JSON.parse(source),
@@ -19,6 +20,8 @@ class JSONFixturesTest < Test::Unit::TestCase
19
20
  raise e
20
21
  end
21
22
  end
23
+ ensure
24
+ $VERBOSE = verbose_bak
22
25
  end
23
26
 
24
27
  def test_failing
@@ -48,36 +48,6 @@ EOT
48
48
  $VERBOSE = v
49
49
  end
50
50
 
51
- def test_remove_const_segv
52
- return if RUBY_ENGINE == 'jruby'
53
- stress = GC.stress
54
- const = JSON::SAFE_STATE_PROTOTYPE.dup
55
-
56
- bignum_too_long_to_embed_as_string = 1234567890123456789012345
57
- expect = bignum_too_long_to_embed_as_string.to_s
58
- GC.stress = true
59
-
60
- 10.times do |i|
61
- tmp = bignum_too_long_to_embed_as_string.to_json
62
- raise "'\#{expect}' is expected, but '\#{tmp}'" unless tmp == expect
63
- end
64
-
65
- silence do
66
- JSON.const_set :SAFE_STATE_PROTOTYPE, nil
67
- end
68
-
69
- 10.times do |i|
70
- assert_raise TypeError do
71
- bignum_too_long_to_embed_as_string.to_json
72
- end
73
- end
74
- ensure
75
- GC.stress = stress
76
- silence do
77
- JSON.const_set :SAFE_STATE_PROTOTYPE, const
78
- end
79
- end if JSON.const_defined?("Ext")
80
-
81
51
  def test_generate
82
52
  json = generate(@hash)
83
53
  assert_equal(parse(@json2), parse(json))
@@ -93,6 +63,11 @@ EOT
93
63
  end
94
64
 
95
65
  def test_generate_pretty
66
+ json = pretty_generate({})
67
+ assert_equal(<<'EOT'.chomp, json)
68
+ {
69
+ }
70
+ EOT
96
71
  json = pretty_generate(@hash)
97
72
  # hashes aren't (insertion) ordered on every ruby implementation
98
73
  # assert_equal(@json3, json)
@@ -167,13 +142,14 @@ EOT
167
142
  end
168
143
 
169
144
  def test_pretty_state
170
- state = PRETTY_STATE_PROTOTYPE.dup
145
+ state = JSON.create_pretty_state
171
146
  assert_equal({
172
147
  :allow_nan => false,
173
148
  :array_nl => "\n",
174
149
  :ascii_only => false,
175
150
  :buffer_initial_length => 1024,
176
151
  :depth => 0,
152
+ :escape_slash => false,
177
153
  :indent => " ",
178
154
  :max_nesting => 100,
179
155
  :object_nl => "\n",
@@ -183,13 +159,14 @@ EOT
183
159
  end
184
160
 
185
161
  def test_safe_state
186
- state = SAFE_STATE_PROTOTYPE.dup
162
+ state = JSON::State.new
187
163
  assert_equal({
188
164
  :allow_nan => false,
189
165
  :array_nl => "",
190
166
  :ascii_only => false,
191
167
  :buffer_initial_length => 1024,
192
168
  :depth => 0,
169
+ :escape_slash => false,
193
170
  :indent => "",
194
171
  :max_nesting => 100,
195
172
  :object_nl => "",
@@ -199,13 +176,14 @@ EOT
199
176
  end
200
177
 
201
178
  def test_fast_state
202
- state = FAST_STATE_PROTOTYPE.dup
179
+ state = JSON.create_fast_state
203
180
  assert_equal({
204
181
  :allow_nan => false,
205
182
  :array_nl => "",
206
183
  :ascii_only => false,
207
184
  :buffer_initial_length => 1024,
208
185
  :depth => 0,
186
+ :escape_slash => false,
209
187
  :indent => "",
210
188
  :max_nesting => 0,
211
189
  :object_nl => "",
@@ -234,12 +212,8 @@ EOT
234
212
 
235
213
  def test_depth
236
214
  ary = []; ary << ary
237
- assert_equal 0, JSON::SAFE_STATE_PROTOTYPE.depth
238
215
  assert_raise(JSON::NestingError) { generate(ary) }
239
- assert_equal 0, JSON::SAFE_STATE_PROTOTYPE.depth
240
- assert_equal 0, JSON::PRETTY_STATE_PROTOTYPE.depth
241
216
  assert_raise(JSON::NestingError) { JSON.pretty_generate(ary) }
242
- assert_equal 0, JSON::PRETTY_STATE_PROTOTYPE.depth
243
217
  s = JSON.state.new
244
218
  assert_equal 0, s.depth
245
219
  assert_raise(JSON::NestingError) { ary.to_json(s) }
@@ -258,7 +232,7 @@ EOT
258
232
  end
259
233
 
260
234
  def test_gc
261
- if respond_to?(:assert_in_out_err)
235
+ if respond_to?(:assert_in_out_err) && !(RUBY_PLATFORM =~ /java/)
262
236
  assert_in_out_err(%w[-rjson --disable-gems], <<-EOS, [], [])
263
237
  bignum_too_long_to_embed_as_string = 1234567890123456789012345
264
238
  expect = bignum_too_long_to_embed_as_string.to_s
@@ -394,6 +368,10 @@ EOT
394
368
  json = '["/"]'
395
369
  assert_equal json, generate(data)
396
370
  #
371
+ data = [ '/' ]
372
+ json = '["\/"]'
373
+ assert_equal json, generate(data, :escape_slash => true)
374
+ #
397
375
  data = ['"']
398
376
  json = '["\""]'
399
377
  assert_equal json, generate(data)
@@ -218,6 +218,17 @@ class JSONParserTest < Test::Unit::TestCase
218
218
  end
219
219
  end
220
220
 
221
+ def test_freeze
222
+ assert_predicate parse('{}', :freeze => true), :frozen?
223
+ assert_predicate parse('[]', :freeze => true), :frozen?
224
+ assert_predicate parse('"foo"', :freeze => true), :frozen?
225
+
226
+ if string_deduplication_available?
227
+ assert_same(-'foo', parse('"foo"', :freeze => true))
228
+ assert_same(-'foo', parse('{"foo": 1}', :freeze => true).keys.first)
229
+ end
230
+ end
231
+
221
232
  def test_parse_comments
222
233
  json = <<EOT
223
234
  {
@@ -293,6 +304,10 @@ EOT
293
304
  json = '["\\\'"]'
294
305
  data = ["'"]
295
306
  assert_equal data, parse(json)
307
+
308
+ json = '["\/"]'
309
+ data = [ '/' ]
310
+ assert_equal data, parse(json)
296
311
  end
297
312
 
298
313
  class SubArray < Array
@@ -464,6 +479,16 @@ EOT
464
479
 
465
480
  private
466
481
 
482
+ def string_deduplication_available?
483
+ r1 = rand.to_s
484
+ r2 = r1.dup
485
+ begin
486
+ (-r1).equal?(-r2)
487
+ rescue NoMethodError
488
+ false # No String#-@
489
+ end
490
+ end
491
+
467
492
  def assert_equal_float(expected, actual, delta = 1e-2)
468
493
  Array === expected and expected = expected.first
469
494
  Array === actual and actual = actual.first
@@ -0,0 +1,763 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Test
4
+ module Unit
5
+ module Assertions
6
+ def _assertions= n # :nodoc:
7
+ @_assertions = n
8
+ end
9
+
10
+ def _assertions # :nodoc:
11
+ @_assertions ||= 0
12
+ end
13
+
14
+ ##
15
+ # Returns a proc that will output +msg+ along with the default message.
16
+
17
+ def message msg = nil, ending = nil, &default
18
+ proc {
19
+ msg = msg.call.chomp(".") if Proc === msg
20
+ custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
21
+ "#{custom_message}#{default.call}#{ending || "."}"
22
+ }
23
+ end
24
+ end
25
+
26
+ module CoreAssertions
27
+ if defined?(MiniTest)
28
+ require_relative '../../envutil'
29
+ # for ruby core testing
30
+ include MiniTest::Assertions
31
+
32
+ # Compatibility hack for assert_raise
33
+ Test::Unit::AssertionFailedError = MiniTest::Assertion
34
+ else
35
+ module MiniTest
36
+ class Assertion < Exception; end
37
+ class Skip < Assertion; end
38
+ end
39
+
40
+ require 'pp'
41
+ require_relative 'envutil'
42
+ include Test::Unit::Assertions
43
+ end
44
+
45
+ def mu_pp(obj) #:nodoc:
46
+ obj.pretty_inspect.chomp
47
+ end
48
+
49
+ def assert_file
50
+ AssertFile
51
+ end
52
+
53
+ FailDesc = proc do |status, message = "", out = ""|
54
+ now = Time.now
55
+ proc do
56
+ EnvUtil.failure_description(status, now, message, out)
57
+ end
58
+ end
59
+
60
+ def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil,
61
+ success: nil, **opt)
62
+ args = Array(args).dup
63
+ args.insert((Hash === args[0] ? 1 : 0), '--disable=gems')
64
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt)
65
+ desc = FailDesc[status, message, stderr]
66
+ if block_given?
67
+ raise "test_stdout ignored, use block only or without block" if test_stdout != []
68
+ raise "test_stderr ignored, use block only or without block" if test_stderr != []
69
+ yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status)
70
+ else
71
+ all_assertions(desc) do |a|
72
+ [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act|
73
+ a.for(key) do
74
+ if exp.is_a?(Regexp)
75
+ assert_match(exp, act)
76
+ elsif exp.all? {|e| String === e}
77
+ assert_equal(exp, act.lines.map {|l| l.chomp })
78
+ else
79
+ assert_pattern_list(exp, act)
80
+ end
81
+ end
82
+ end
83
+ unless success.nil?
84
+ a.for("success?") do
85
+ if success
86
+ assert_predicate(status, :success?)
87
+ else
88
+ assert_not_predicate(status, :success?)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ status
94
+ end
95
+ end
96
+
97
+ if defined?(RubyVM::InstructionSequence)
98
+ def syntax_check(code, fname, line)
99
+ code = code.dup.force_encoding(Encoding::UTF_8)
100
+ RubyVM::InstructionSequence.compile(code, fname, fname, line)
101
+ :ok
102
+ ensure
103
+ raise if SyntaxError === $!
104
+ end
105
+ else
106
+ def syntax_check(code, fname, line)
107
+ code = code.b
108
+ code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
109
+ "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
110
+ }
111
+ code = code.force_encoding(Encoding::UTF_8)
112
+ catch {|tag| eval(code, binding, fname, line - 1)}
113
+ end
114
+ end
115
+
116
+ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt)
117
+ # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail
118
+ pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?
119
+
120
+ require_relative '../../memory_status'
121
+ raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status)
122
+
123
+ token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m"
124
+ token_dump = token.dump
125
+ token_re = Regexp.quote(token)
126
+ envs = args.shift if Array === args and Hash === args.first
127
+ args = [
128
+ "--disable=gems",
129
+ "-r", File.expand_path("../../../memory_status", __FILE__),
130
+ *args,
131
+ "-v", "-",
132
+ ]
133
+ if defined? Memory::NO_MEMORY_LEAK_ENVS then
134
+ envs ||= {}
135
+ newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break }
136
+ envs = newenvs if newenvs
137
+ end
138
+ args.unshift(envs) if envs
139
+ cmd = [
140
+ 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}',
141
+ prepare,
142
+ 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")',
143
+ '$initial_size = $initial_status.size',
144
+ code,
145
+ 'GC.start',
146
+ ].join("\n")
147
+ _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt)
148
+ before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1)
149
+ after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1)
150
+ assert(status.success?, FailDesc[status, message, err])
151
+ ([:size, (rss && :rss)] & after.members).each do |n|
152
+ b = before[n]
153
+ a = after[n]
154
+ next unless a > 0 and b > 0
155
+ assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"})
156
+ end
157
+ rescue LoadError
158
+ pend
159
+ end
160
+
161
+ # :call-seq:
162
+ # assert_nothing_raised( *args, &block )
163
+ #
164
+ #If any exceptions are given as arguments, the assertion will
165
+ #fail if one of those exceptions are raised. Otherwise, the test fails
166
+ #if any exceptions are raised.
167
+ #
168
+ #The final argument may be a failure message.
169
+ #
170
+ # assert_nothing_raised RuntimeError do
171
+ # raise Exception #Assertion passes, Exception is not a RuntimeError
172
+ # end
173
+ #
174
+ # assert_nothing_raised do
175
+ # raise Exception #Assertion fails
176
+ # end
177
+ def assert_nothing_raised(*args)
178
+ self._assertions += 1
179
+ if Module === args.last
180
+ msg = nil
181
+ else
182
+ msg = args.pop
183
+ end
184
+ begin
185
+ line = __LINE__; yield
186
+ rescue MiniTest::Skip
187
+ raise
188
+ rescue Exception => e
189
+ bt = e.backtrace
190
+ as = e.instance_of?(MiniTest::Assertion)
191
+ if as
192
+ ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o
193
+ bt.reject! {|ln| ans =~ ln}
194
+ end
195
+ if ((args.empty? && !as) ||
196
+ args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a })
197
+ msg = message(msg) {
198
+ "Exception raised:\n<#{mu_pp(e)}>\n" +
199
+ "Backtrace:\n" +
200
+ e.backtrace.map{|frame| " #{frame}"}.join("\n")
201
+ }
202
+ raise MiniTest::Assertion, msg.call, bt
203
+ else
204
+ raise
205
+ end
206
+ end
207
+ end
208
+
209
+ def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil)
210
+ fname ||= caller_locations(2, 1)[0]
211
+ mesg ||= fname.to_s
212
+ verbose, $VERBOSE = $VERBOSE, verbose
213
+ case
214
+ when Array === fname
215
+ fname, line = *fname
216
+ when defined?(fname.path) && defined?(fname.lineno)
217
+ fname, line = fname.path, fname.lineno
218
+ else
219
+ line = 1
220
+ end
221
+ yield(code, fname, line, message(mesg) {
222
+ if code.end_with?("\n")
223
+ "```\n#{code}```\n"
224
+ else
225
+ "```\n#{code}\n```\n""no-newline"
226
+ end
227
+ })
228
+ ensure
229
+ $VERBOSE = verbose
230
+ end
231
+
232
+ def assert_valid_syntax(code, *args, **opt)
233
+ prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg|
234
+ yield if defined?(yield)
235
+ assert_nothing_raised(SyntaxError, mesg) do
236
+ assert_equal(:ok, syntax_check(src, fname, line), mesg)
237
+ end
238
+ end
239
+ end
240
+
241
+ def assert_normal_exit(testsrc, message = '', child_env: nil, **opt)
242
+ assert_valid_syntax(testsrc, caller_locations(1, 1)[0])
243
+ if child_env
244
+ child_env = [child_env]
245
+ else
246
+ child_env = []
247
+ end
248
+ out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt)
249
+ assert !status.signaled?, FailDesc[status, message, out]
250
+ end
251
+
252
+ def assert_ruby_status(args, test_stdin="", message=nil, **opt)
253
+ out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt)
254
+ desc = FailDesc[status, message, out]
255
+ assert(!status.signaled?, desc)
256
+ message ||= "ruby exit status is not success:"
257
+ assert(status.success?, desc)
258
+ end
259
+
260
+ ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM")
261
+
262
+ def separated_runner(out = nil)
263
+ out = out ? IO.new(out, 'w') : STDOUT
264
+ at_exit {
265
+ out.puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}"
266
+ }
267
+ Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner)
268
+ end
269
+
270
+ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt)
271
+ unless file and line
272
+ loc, = caller_locations(1,1)
273
+ file ||= loc.path
274
+ line ||= loc.lineno
275
+ end
276
+ capture_stdout = true
277
+ unless /mswin|mingw/ =~ RUBY_PLATFORM
278
+ capture_stdout = false
279
+ opt[:out] = MiniTest::Unit.output if defined?(MiniTest::Unit)
280
+ res_p, res_c = IO.pipe
281
+ opt[res_c.fileno] = res_c.fileno
282
+ end
283
+ src = <<eom
284
+ # -*- coding: #{line += __LINE__; src.encoding}; -*-
285
+ BEGIN {
286
+ require "test/unit";include Test::Unit::Assertions;require #{(__dir__ + "/core_assertions").dump};include Test::Unit::CoreAssertions
287
+ separated_runner #{res_c&.fileno}
288
+ }
289
+ #{line -= __LINE__; src}
290
+ eom
291
+ args = args.dup
292
+ args.insert((Hash === args.first ? 1 : 0), "-w", "--disable=gems", *$:.map {|l| "-I#{l}"})
293
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, src, capture_stdout, true, **opt)
294
+ ensure
295
+ if res_c
296
+ res_c.close
297
+ res = res_p.read
298
+ res_p.close
299
+ else
300
+ res = stdout
301
+ end
302
+ raise if $!
303
+ abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig))
304
+ assert(!abort, FailDesc[status, nil, stderr])
305
+ self._assertions += res[/^assertions=(\d+)/, 1].to_i
306
+ begin
307
+ res = Marshal.load(res.unpack1("m"))
308
+ rescue => marshal_error
309
+ ignore_stderr = nil
310
+ res = nil
311
+ end
312
+ if res and !(SystemExit === res)
313
+ if bt = res.backtrace
314
+ bt.each do |l|
315
+ l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"}
316
+ end
317
+ bt.concat(caller)
318
+ else
319
+ res.set_backtrace(caller)
320
+ end
321
+ raise res
322
+ end
323
+
324
+ # really is it succeed?
325
+ unless ignore_stderr
326
+ # the body of assert_separately must not output anything to detect error
327
+ assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr])
328
+ end
329
+ assert(status.success?, FailDesc[status, "assert_separately failed", stderr])
330
+ raise marshal_error if marshal_error
331
+ end
332
+
333
+ # Run Ractor-related test without influencing the main test suite
334
+ def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt)
335
+ return unless defined?(Ractor)
336
+
337
+ require = "require #{require.inspect}" if require
338
+ if require_relative
339
+ dir = File.dirname(caller_locations[0,1][0].absolute_path)
340
+ full_path = File.expand_path(require_relative, dir)
341
+ require = "#{require}; require #{full_path.inspect}"
342
+ end
343
+
344
+ assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt)
345
+ #{require}
346
+ previous_verbose = $VERBOSE
347
+ $VERBOSE = nil
348
+ Ractor.new {} # trigger initial warning
349
+ $VERBOSE = previous_verbose
350
+ #{src}
351
+ RUBY
352
+ end
353
+
354
+ # :call-seq:
355
+ # assert_throw( tag, failure_message = nil, &block )
356
+ #
357
+ #Fails unless the given block throws +tag+, returns the caught
358
+ #value otherwise.
359
+ #
360
+ #An optional failure message may be provided as the final argument.
361
+ #
362
+ # tag = Object.new
363
+ # assert_throw(tag, "#{tag} was not thrown!") do
364
+ # throw tag
365
+ # end
366
+ def assert_throw(tag, msg = nil)
367
+ ret = catch(tag) do
368
+ begin
369
+ yield(tag)
370
+ rescue UncaughtThrowError => e
371
+ thrown = e.tag
372
+ end
373
+ msg = message(msg) {
374
+ "Expected #{mu_pp(tag)} to have been thrown"\
375
+ "#{%Q[, not #{thrown}] if thrown}"
376
+ }
377
+ assert(false, msg)
378
+ end
379
+ assert(true)
380
+ ret
381
+ end
382
+
383
+ # :call-seq:
384
+ # assert_raise( *args, &block )
385
+ #
386
+ #Tests if the given block raises an exception. Acceptable exception
387
+ #types may be given as optional arguments. If the last argument is a
388
+ #String, it will be used as the error message.
389
+ #
390
+ # assert_raise do #Fails, no Exceptions are raised
391
+ # end
392
+ #
393
+ # assert_raise NameError do
394
+ # puts x #Raises NameError, so assertion succeeds
395
+ # end
396
+ def assert_raise(*exp, &b)
397
+ case exp.last
398
+ when String, Proc
399
+ msg = exp.pop
400
+ end
401
+
402
+ begin
403
+ yield
404
+ rescue MiniTest::Skip => e
405
+ return e if exp.include? MiniTest::Skip
406
+ raise e
407
+ rescue Exception => e
408
+ expected = exp.any? { |ex|
409
+ if ex.instance_of? Module then
410
+ e.kind_of? ex
411
+ else
412
+ e.instance_of? ex
413
+ end
414
+ }
415
+
416
+ assert expected, proc {
417
+ flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"})
418
+ }
419
+
420
+ return e
421
+ ensure
422
+ unless e
423
+ exp = exp.first if exp.size == 1
424
+
425
+ flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"})
426
+ end
427
+ end
428
+ end
429
+
430
+ # :call-seq:
431
+ # assert_raise_with_message(exception, expected, msg = nil, &block)
432
+ #
433
+ #Tests if the given block raises an exception with the expected
434
+ #message.
435
+ #
436
+ # assert_raise_with_message(RuntimeError, "foo") do
437
+ # nil #Fails, no Exceptions are raised
438
+ # end
439
+ #
440
+ # assert_raise_with_message(RuntimeError, "foo") do
441
+ # raise ArgumentError, "foo" #Fails, different Exception is raised
442
+ # end
443
+ #
444
+ # assert_raise_with_message(RuntimeError, "foo") do
445
+ # raise "bar" #Fails, RuntimeError is raised but the message differs
446
+ # end
447
+ #
448
+ # assert_raise_with_message(RuntimeError, "foo") do
449
+ # raise "foo" #Raises RuntimeError with the message, so assertion succeeds
450
+ # end
451
+ def assert_raise_with_message(exception, expected, msg = nil, &block)
452
+ case expected
453
+ when String
454
+ assert = :assert_equal
455
+ when Regexp
456
+ assert = :assert_match
457
+ else
458
+ raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}"
459
+ end
460
+
461
+ ex = m = nil
462
+ EnvUtil.with_default_internal(expected.encoding) do
463
+ ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do
464
+ yield
465
+ end
466
+ m = ex.message
467
+ end
468
+ msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"}
469
+
470
+ if assert == :assert_equal
471
+ assert_equal(expected, m, msg)
472
+ else
473
+ msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" }
474
+ assert expected =~ m, msg
475
+ block.binding.eval("proc{|_|$~=_}").call($~)
476
+ end
477
+ ex
478
+ end
479
+
480
+ MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc:
481
+
482
+ # :call-seq:
483
+ # assert(test, [failure_message])
484
+ #
485
+ #Tests if +test+ is true.
486
+ #
487
+ #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used
488
+ #as the failure message. Otherwise, the result of calling +msg+ will be
489
+ #used as the message if the assertion fails.
490
+ #
491
+ #If no +msg+ is given, a default message will be used.
492
+ #
493
+ # assert(false, "This was expected to be true")
494
+ def assert(test, *msgs)
495
+ case msg = msgs.first
496
+ when String, Proc
497
+ when nil
498
+ msgs.shift
499
+ else
500
+ bt = caller.reject { |s| s.start_with?(MINI_DIR) }
501
+ raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt
502
+ end unless msgs.empty?
503
+ super
504
+ end
505
+
506
+ # :call-seq:
507
+ # assert_respond_to( object, method, failure_message = nil )
508
+ #
509
+ #Tests if the given Object responds to +method+.
510
+ #
511
+ #An optional failure message may be provided as the final argument.
512
+ #
513
+ # assert_respond_to("hello", :reverse) #Succeeds
514
+ # assert_respond_to("hello", :does_not_exist) #Fails
515
+ def assert_respond_to(obj, (meth, *priv), msg = nil)
516
+ unless priv.empty?
517
+ msg = message(msg) {
518
+ "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}"
519
+ }
520
+ return assert obj.respond_to?(meth, *priv), msg
521
+ end
522
+ #get rid of overcounting
523
+ if caller_locations(1, 1)[0].path.start_with?(MINI_DIR)
524
+ return if obj.respond_to?(meth)
525
+ end
526
+ super(obj, meth, msg)
527
+ end
528
+
529
+ # :call-seq:
530
+ # assert_not_respond_to( object, method, failure_message = nil )
531
+ #
532
+ #Tests if the given Object does not respond to +method+.
533
+ #
534
+ #An optional failure message may be provided as the final argument.
535
+ #
536
+ # assert_not_respond_to("hello", :reverse) #Fails
537
+ # assert_not_respond_to("hello", :does_not_exist) #Succeeds
538
+ def assert_not_respond_to(obj, (meth, *priv), msg = nil)
539
+ unless priv.empty?
540
+ msg = message(msg) {
541
+ "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}"
542
+ }
543
+ return assert !obj.respond_to?(meth, *priv), msg
544
+ end
545
+ #get rid of overcounting
546
+ if caller_locations(1, 1)[0].path.start_with?(MINI_DIR)
547
+ return unless obj.respond_to?(meth)
548
+ end
549
+ refute_respond_to(obj, meth, msg)
550
+ end
551
+
552
+ # pattern_list is an array which contains regexp and :*.
553
+ # :* means any sequence.
554
+ #
555
+ # pattern_list is anchored.
556
+ # Use [:*, regexp, :*] for non-anchored match.
557
+ def assert_pattern_list(pattern_list, actual, message=nil)
558
+ rest = actual
559
+ anchored = true
560
+ pattern_list.each_with_index {|pattern, i|
561
+ if pattern == :*
562
+ anchored = false
563
+ else
564
+ if anchored
565
+ match = /\A#{pattern}/.match(rest)
566
+ else
567
+ match = pattern.match(rest)
568
+ end
569
+ unless match
570
+ msg = message(msg) {
571
+ expect_msg = "Expected #{mu_pp pattern}\n"
572
+ if /\n[^\n]/ =~ rest
573
+ actual_mesg = +"to match\n"
574
+ rest.scan(/.*\n+/) {
575
+ actual_mesg << ' ' << $&.inspect << "+\n"
576
+ }
577
+ actual_mesg.sub!(/\+\n\z/, '')
578
+ else
579
+ actual_mesg = "to match " + mu_pp(rest)
580
+ end
581
+ actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters"
582
+ expect_msg + actual_mesg
583
+ }
584
+ assert false, msg
585
+ end
586
+ rest = match.post_match
587
+ anchored = true
588
+ end
589
+ }
590
+ if anchored
591
+ assert_equal("", rest)
592
+ end
593
+ end
594
+
595
+ def assert_warning(pat, msg = nil)
596
+ result = nil
597
+ stderr = EnvUtil.with_default_internal(pat.encoding) {
598
+ EnvUtil.verbose_warning {
599
+ result = yield
600
+ }
601
+ }
602
+ msg = message(msg) {diff pat, stderr}
603
+ assert(pat === stderr, msg)
604
+ result
605
+ end
606
+
607
+ def assert_warn(*args)
608
+ assert_warning(*args) {$VERBOSE = false; yield}
609
+ end
610
+
611
+ def assert_deprecated_warning(mesg = /deprecated/)
612
+ assert_warning(mesg) do
613
+ Warning[:deprecated] = true
614
+ yield
615
+ end
616
+ end
617
+
618
+ def assert_deprecated_warn(mesg = /deprecated/)
619
+ assert_warn(mesg) do
620
+ Warning[:deprecated] = true
621
+ yield
622
+ end
623
+ end
624
+
625
+ class << (AssertFile = Struct.new(:failure_message).new)
626
+ include CoreAssertions
627
+ def assert_file_predicate(predicate, *args)
628
+ if /\Anot_/ =~ predicate
629
+ predicate = $'
630
+ neg = " not"
631
+ end
632
+ result = File.__send__(predicate, *args)
633
+ result = !result if neg
634
+ mesg = "Expected file ".dup << args.shift.inspect
635
+ mesg << "#{neg} to be #{predicate}"
636
+ mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty?
637
+ mesg << " #{failure_message}" if failure_message
638
+ assert(result, mesg)
639
+ end
640
+ alias method_missing assert_file_predicate
641
+
642
+ def for(message)
643
+ clone.tap {|a| a.failure_message = message}
644
+ end
645
+ end
646
+
647
+ class AllFailures
648
+ attr_reader :failures
649
+
650
+ def initialize
651
+ @count = 0
652
+ @failures = {}
653
+ end
654
+
655
+ def for(key)
656
+ @count += 1
657
+ yield
658
+ rescue Exception => e
659
+ @failures[key] = [@count, e]
660
+ end
661
+
662
+ def foreach(*keys)
663
+ keys.each do |key|
664
+ @count += 1
665
+ begin
666
+ yield key
667
+ rescue Exception => e
668
+ @failures[key] = [@count, e]
669
+ end
670
+ end
671
+ end
672
+
673
+ def message
674
+ i = 0
675
+ total = @count.to_s
676
+ fmt = "%#{total.size}d"
677
+ @failures.map {|k, (n, v)|
678
+ v = v.message
679
+ "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}"
680
+ }.join("\n")
681
+ end
682
+
683
+ def pass?
684
+ @failures.empty?
685
+ end
686
+ end
687
+
688
+ # threads should respond to shift method.
689
+ # Array can be used.
690
+ def assert_join_threads(threads, message = nil)
691
+ errs = []
692
+ values = []
693
+ while th = threads.shift
694
+ begin
695
+ values << th.value
696
+ rescue Exception
697
+ errs << [th, $!]
698
+ th = nil
699
+ end
700
+ end
701
+ values
702
+ ensure
703
+ if th&.alive?
704
+ th.raise(Timeout::Error.new)
705
+ th.join rescue errs << [th, $!]
706
+ end
707
+ if !errs.empty?
708
+ msg = "exceptions on #{errs.length} threads:\n" +
709
+ errs.map {|t, err|
710
+ "#{t.inspect}:\n" +
711
+ RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message
712
+ }.join("\n---\n")
713
+ if message
714
+ msg = "#{message}\n#{msg}"
715
+ end
716
+ raise MiniTest::Assertion, msg
717
+ end
718
+ end
719
+
720
+ def assert_all_assertions(msg = nil)
721
+ all = AllFailures.new
722
+ yield all
723
+ ensure
724
+ assert(all.pass?, message(msg) {all.message.chomp(".")})
725
+ end
726
+ alias all_assertions assert_all_assertions
727
+
728
+ def message(msg = nil, *args, &default) # :nodoc:
729
+ if Proc === msg
730
+ super(nil, *args) do
731
+ ary = [msg.call, (default.call if default)].compact.reject(&:empty?)
732
+ if 1 < ary.length
733
+ ary[0...-1] = ary[0...-1].map {|str| str.sub(/(?<!\.)\z/, '.') }
734
+ end
735
+ begin
736
+ ary.join("\n")
737
+ rescue Encoding::CompatibilityError
738
+ ary.map(&:b).join("\n")
739
+ end
740
+ end
741
+ else
742
+ super
743
+ end
744
+ end
745
+
746
+ def diff(exp, act)
747
+ require 'pp'
748
+ q = PP.new(+"")
749
+ q.guard_inspect_key do
750
+ q.group(2, "expected: ") do
751
+ q.pp exp
752
+ end
753
+ q.text q.newline
754
+ q.group(2, "actual: ") do
755
+ q.pp act
756
+ end
757
+ q.flush
758
+ end
759
+ q.output
760
+ end
761
+ end
762
+ end
763
+ end