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.
- checksums.yaml +4 -4
- data/CHANGES.md +22 -0
- data/LICENSE +56 -0
- data/VERSION +1 -1
- data/ext/json/ext/generator/generator.c +60 -11
- data/ext/json/ext/generator/generator.h +5 -2
- data/ext/json/ext/parser/extconf.rb +25 -0
- data/ext/json/ext/parser/parser.c +110 -68
- data/ext/json/ext/parser/parser.h +1 -0
- data/ext/json/ext/parser/parser.rl +67 -25
- data/ext/json/extconf.rb +1 -0
- data/json.gemspec +11 -77
- data/lib/json.rb +171 -0
- data/lib/json/add/complex.rb +0 -1
- data/lib/json/add/rational.rb +0 -1
- data/lib/json/common.rb +240 -228
- data/lib/json/pure/generator.rb +28 -8
- data/lib/json/pure/parser.rb +20 -2
- data/lib/json/version.rb +1 -1
- data/tests/fixtures/fail29.json +1 -0
- data/tests/fixtures/fail30.json +1 -0
- data/tests/fixtures/fail31.json +1 -0
- data/tests/fixtures/fail32.json +1 -0
- data/tests/json_addition_test.rb +0 -4
- data/tests/json_common_interface_test.rb +43 -0
- data/tests/json_fixtures_test.rb +3 -0
- data/tests/json_generator_test.rb +16 -38
- data/tests/json_parser_test.rb +25 -0
- data/tests/lib/core_assertions.rb +763 -0
- data/tests/lib/envutil.rb +365 -0
- data/tests/lib/find_executable.rb +22 -0
- data/tests/lib/helper.rb +4 -0
- data/tests/ractor_test.rb +30 -0
- data/tests/test_helper.rb +3 -3
- metadata +16 -37
- data/.gitignore +0 -18
- data/.travis.yml +0 -26
- data/README-json-jruby.md +0 -33
- data/Rakefile +0 -334
- data/diagrams/.keep +0 -0
- data/install.rb +0 -23
- data/java/src/json/ext/ByteListTranscoder.java +0 -166
- data/java/src/json/ext/Generator.java +0 -466
- data/java/src/json/ext/GeneratorMethods.java +0 -231
- data/java/src/json/ext/GeneratorService.java +0 -42
- data/java/src/json/ext/GeneratorState.java +0 -490
- data/java/src/json/ext/OptionsReader.java +0 -113
- data/java/src/json/ext/Parser.java +0 -2362
- data/java/src/json/ext/Parser.rl +0 -893
- data/java/src/json/ext/ParserService.java +0 -34
- data/java/src/json/ext/RuntimeInfo.java +0 -116
- data/java/src/json/ext/StringDecoder.java +0 -166
- data/java/src/json/ext/StringEncoder.java +0 -111
- data/java/src/json/ext/Utils.java +0 -88
- data/json-java.gemspec +0 -37
- data/json_pure.gemspec +0 -33
- data/references/rfc7159.txt +0 -899
- data/tools/diff.sh +0 -18
- data/tools/fuzz.rb +0 -131
- data/tools/server.rb +0 -62
data/lib/json/pure/generator.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
318
|
-
|
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
|
|
data/lib/json/pure/parser.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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)
|
data/lib/json/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
{
|
@@ -0,0 +1 @@
|
|
1
|
+
[
|
@@ -0,0 +1 @@
|
|
1
|
+
[1, 2, 3,
|
@@ -0,0 +1 @@
|
|
1
|
+
{"foo": "bar"
|
data/tests/json_addition_test.rb
CHANGED
@@ -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
|
data/tests/json_fixtures_test.rb
CHANGED
@@ -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 =
|
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 =
|
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 =
|
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)
|
data/tests/json_parser_test.rb
CHANGED
@@ -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
|