appmap 0.26.1 → 0.32.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -3
  3. data/CHANGELOG.md +37 -0
  4. data/README.md +170 -29
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +56 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +21 -28
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +89 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +28 -19
  14. data/lib/appmap/hook.rb +56 -128
  15. data/lib/appmap/hook/method.rb +78 -0
  16. data/lib/appmap/metadata.rb +62 -0
  17. data/lib/appmap/middleware/remote_recording.rb +2 -6
  18. data/lib/appmap/minitest.rb +141 -0
  19. data/lib/appmap/rails/action_handler.rb +7 -7
  20. data/lib/appmap/rails/sql_handler.rb +10 -8
  21. data/lib/appmap/railtie.rb +2 -2
  22. data/lib/appmap/record.rb +27 -0
  23. data/lib/appmap/rspec.rb +20 -38
  24. data/lib/appmap/trace.rb +19 -11
  25. data/lib/appmap/util.rb +59 -0
  26. data/lib/appmap/version.rb +1 -1
  27. data/package-lock.json +3 -3
  28. data/spec/abstract_controller4_base_spec.rb +1 -1
  29. data/spec/abstract_controller_base_spec.rb +9 -2
  30. data/spec/config_spec.rb +3 -3
  31. data/spec/fixtures/hook/compare.rb +7 -0
  32. data/spec/fixtures/hook/singleton_method.rb +54 -0
  33. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  34. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  35. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  36. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  37. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  38. data/spec/hook_spec.rb +228 -53
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/record_sql_rails_pg_spec.rb +56 -33
  41. data/spec/rspec_feature_metadata_spec.rb +2 -0
  42. data/spec/spec_helper.rb +4 -0
  43. data/spec/util_spec.rb +21 -0
  44. data/test/cli_test.rb +4 -4
  45. data/test/cucumber_test.rb +72 -0
  46. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  47. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  48. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  51. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  52. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  53. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  54. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  55. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  57. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  58. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  59. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  60. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  61. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  62. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  63. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  64. data/test/fixtures/process_recorder/appmap.yml +3 -0
  65. data/test/fixtures/process_recorder/hello.rb +9 -0
  66. data/test/minitest_test.rb +38 -0
  67. data/test/record_process_test.rb +35 -0
  68. data/test/test_helper.rb +1 -0
  69. metadata +39 -3
  70. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'rails_spec_helper'
4
4
  require 'active_support/core_ext'
5
- require 'appmap/hook'
5
+ require 'appmap/config'
6
6
 
7
- describe AppMap::Hook::Config do
7
+ describe AppMap::Config, docker: false do
8
8
  it 'loads from a Hash' do
9
9
  config_data = {
10
10
  name: 'test',
@@ -18,7 +18,7 @@ describe AppMap::Hook::Config do
18
18
  }
19
19
  ]
20
20
  }.deep_stringify_keys!
21
- config = AppMap::Hook::Config.load(config_data)
21
+ config = AppMap::Config.load(config_data)
22
22
 
23
23
  expect(config.to_h.deep_stringify_keys!).to eq(config_data)
24
24
  end
@@ -0,0 +1,7 @@
1
+ require 'active_support/security_utils'
2
+
3
+ class Compare
4
+ def self.compare(s1, s2)
5
+ ActiveSupport::SecurityUtils.secure_compare(s1, s2)
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SingletonMethod
4
+ class << self
5
+ def say_default
6
+ 'default'
7
+ end
8
+ end
9
+
10
+ def SingletonMethod.say_class_defined
11
+ 'defined with explicit class scope'
12
+ end
13
+
14
+ def self.say_self_defined
15
+ 'defined with self class scope'
16
+ end
17
+
18
+ # When called, do_include calls +include+ to bring in the module
19
+ # AddMethod. AddMethod defines a new instance method, which gets
20
+ # added to the singleton class of SingletonMethod.
21
+ def do_include
22
+ class << self
23
+ SingletonMethod.include(AddMethod)
24
+ end
25
+ self
26
+ end
27
+
28
+ def self.new_with_instance_method
29
+ SingletonMethod.new.tap do |m|
30
+ def m.say_instance_defined
31
+ 'defined for an instance'
32
+ end
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ 'Singleton Method fixture'
38
+ end
39
+ end
40
+
41
+ module AddMethod
42
+ def self.included(base)
43
+ base.module_eval do
44
+ define_method "added_method" do
45
+ _added_method
46
+ end
47
+ end
48
+ end
49
+
50
+ def _added_method
51
+ 'defined by including a module'
52
+ end
53
+ end
54
+
@@ -39,6 +39,7 @@ appmap_options = \
39
39
  gem 'appmap', appmap_options
40
40
 
41
41
  group :development, :test do
42
+ gem 'cucumber-rails', require: false
42
43
  gem 'rspec-rails'
43
44
  # Required for Sequel, since without ActiveRecord, the Rails transactional fixture support
44
45
  # isn't activated.
@@ -0,0 +1,13 @@
1
+ Feature: /api/users
2
+
3
+ @appmap-disable
4
+ Scenario: A user can be created
5
+ When I create a user
6
+ Then the response status should be 201
7
+
8
+ Scenario: When a user is created, it should be in the user list
9
+ Given I create a user
10
+ And the response status should be 201
11
+ When I list the users
12
+ Then the response status should be 200
13
+ And the response should include the user
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/rails'
4
+ require 'appmap/cucumber'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ if AppMap::Cucumber.enabled?
4
+ Around('not @appmap-disable') do |scenario, block|
5
+ appmap = AppMap.record do
6
+ block.call
7
+ end
8
+
9
+ AppMap::Cucumber.write_scenario(scenario, appmap)
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ When 'I create a user' do
4
+ @response = post '/api/users', login: 'alice'
5
+ end
6
+
7
+ Then(/the response status should be (\d+)/) do |status|
8
+ expect(@response.status).to eq(status.to_i)
9
+ end
10
+
11
+ When 'I list the users' do
12
+ @response = get '/api/users'
13
+ @users = JSON.parse(@response.body)
14
+ end
15
+
16
+ Then 'the response should include the user' do
17
+ expect(@users.map { |u| u['login'] }).to include('alice')
18
+ end
@@ -5,49 +5,50 @@ require 'appmap/hook'
5
5
  require 'appmap/event'
6
6
  require 'diffy'
7
7
 
8
- describe 'AppMap class Hooking' do
8
+ # Show nulls as the literal +null+, rather than just leaving the field
9
+ # empty. This make some of the expected YAML below easier to
10
+ # understand.
11
+ module ShowYamlNulls
12
+ def visit_NilClass(o)
13
+ @emitter.scalar('null', nil, 'tag:yaml.org,2002:null', true, false, Psych::Nodes::Scalar::ANY)
14
+ end
15
+ end
16
+ Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
17
+
18
+ describe 'AppMap class Hooking', docker: false do
19
+ require 'appmap/util'
9
20
  def collect_events(tracer)
10
21
  [].tap do |events|
11
22
  while tracer.event?
12
23
  events << tracer.next_event.to_h
13
24
  end
14
- end.map do |event|
15
- event.delete(:thread_id)
16
- event.delete(:elapsed)
17
- delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
18
- delete_object_id.call(event[:receiver])
19
- delete_object_id.call(event[:return_value])
20
- (event[:parameters] || []).each(&delete_object_id)
21
- (event[:exceptions] || []).each(&delete_object_id)
22
-
23
- if event[:event] == :return
24
- # These should be removed from the appmap spec
25
- %i[defined_class method_id path lineno static].each do |obsolete_field|
26
- event.delete(obsolete_field)
27
- end
28
- end
29
- event
30
- end.to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
31
26
  end
32
27
 
33
- def invoke_test_file(file, &block)
34
- package = AppMap::Hook::Package.new(file, [])
35
- config = AppMap::Hook::Config.new('hook_spec', [ package ])
36
- AppMap::Hook.hook(config)
37
-
38
- tracer = AppMap.tracing.trace
39
- AppMap::Event.reset_id_counter
40
- begin
41
- load file
42
- yield
43
- ensure
44
- AppMap.tracing.delete(tracer)
28
+ def invoke_test_file(file, setup: nil, &block)
29
+ AppMap.configuration = nil
30
+ package = AppMap::Package.new(file, nil, [])
31
+ config = AppMap::Config.new('hook_spec', [ package ])
32
+ AppMap.configuration = config
33
+ tracer = nil
34
+ AppMap::Hook.new(config).enable do
35
+ setup_result = setup.call if setup
36
+
37
+ tracer = AppMap.tracing.trace
38
+ AppMap::Event.reset_id_counter
39
+ begin
40
+ load file
41
+ yield setup_result
42
+ ensure
43
+ AppMap.tracing.delete(tracer)
44
+ end
45
45
  end
46
+
46
47
  [ config, tracer ]
47
48
  end
48
49
 
49
- def test_hook_behavior(file, events_yaml, &block)
50
- config, tracer = invoke_test_file(file, &block)
50
+ def test_hook_behavior(file, events_yaml, setup: nil, &block)
51
+ config, tracer = invoke_test_file(file, setup: setup, &block)
51
52
 
52
53
  events = collect_events(tracer)
53
54
  expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
@@ -55,6 +56,10 @@ describe 'AppMap class Hooking' do
55
56
  [ config, tracer ]
56
57
  end
57
58
 
59
+ after do
60
+ AppMap.configuration = nil
61
+ end
62
+
58
63
  it 'hooks an instance method that takes no arguments' do
59
64
  events_yaml = <<~YAML
60
65
  ---
@@ -76,7 +81,7 @@ describe 'AppMap class Hooking' do
76
81
  :class: String
77
82
  :value: default
78
83
  YAML
79
- config, tracer = test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
84
+ test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
80
85
  expect(InstanceMethod.new.say_default).to eq('default')
81
86
  end
82
87
  end
@@ -86,14 +91,14 @@ describe 'AppMap class Hooking' do
86
91
  InstanceMethod.new.say_default
87
92
  end
88
93
  expect(tracer.event_methods.to_a.map(&:defined_class)).to eq([ 'InstanceMethod' ])
89
- expect(tracer.event_methods.to_a.map(&:method).map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
94
+ expect(tracer.event_methods.to_a.map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
90
95
  end
91
96
 
92
97
  it 'builds a class map of invoked methods' do
93
- config, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
98
+ _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
94
99
  InstanceMethod.new.say_default
95
100
  end
96
- class_map = AppMap.class_map(config, tracer.event_methods).to_yaml
101
+ class_map = AppMap.class_map(tracer.event_methods).to_yaml
97
102
  expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
98
103
  ---
99
104
  - :name: spec/fixtures/hook/instance_method.rb
@@ -202,7 +207,7 @@ describe 'AppMap class Hooking' do
202
207
  :parameters:
203
208
  - :name: :kw
204
209
  :class: NilClass
205
- :value:
210
+ :value: null
206
211
  :kind: :key
207
212
  :receiver:
208
213
  :class: InstanceMethod
@@ -232,7 +237,7 @@ describe 'AppMap class Hooking' do
232
237
  :parameters:
233
238
  - :name: :block
234
239
  :class: NilClass
235
- :value:
240
+ :value: null
236
241
  :kind: :block
237
242
  :receiver:
238
243
  :class: InstanceMethod
@@ -254,15 +259,15 @@ describe 'AppMap class Hooking' do
254
259
  ---
255
260
  - :id: 1
256
261
  :event: :call
257
- :defined_class: ClassMethod
262
+ :defined_class: SingletonMethod
258
263
  :method_id: say_default
259
- :path: spec/fixtures/hook/class_method.rb
264
+ :path: spec/fixtures/hook/singleton_method.rb
260
265
  :lineno: 5
261
266
  :static: true
262
267
  :parameters: []
263
268
  :receiver:
264
269
  :class: Class
265
- :value: ClassMethod
270
+ :value: SingletonMethod
266
271
  - :id: 2
267
272
  :event: :return
268
273
  :parent_id: 1
@@ -270,8 +275,8 @@ describe 'AppMap class Hooking' do
270
275
  :class: String
271
276
  :value: default
272
277
  YAML
273
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
274
- expect(ClassMethod.say_default).to eq('default')
278
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
279
+ expect(SingletonMethod.say_default).to eq('default')
275
280
  end
276
281
  end
277
282
 
@@ -280,15 +285,15 @@ describe 'AppMap class Hooking' do
280
285
  ---
281
286
  - :id: 1
282
287
  :event: :call
283
- :defined_class: ClassMethod
288
+ :defined_class: SingletonMethod
284
289
  :method_id: say_class_defined
285
- :path: spec/fixtures/hook/class_method.rb
290
+ :path: spec/fixtures/hook/singleton_method.rb
286
291
  :lineno: 10
287
292
  :static: true
288
293
  :parameters: []
289
294
  :receiver:
290
295
  :class: Class
291
- :value: ClassMethod
296
+ :value: SingletonMethod
292
297
  - :id: 2
293
298
  :event: :return
294
299
  :parent_id: 1
@@ -296,8 +301,8 @@ describe 'AppMap class Hooking' do
296
301
  :class: String
297
302
  :value: defined with explicit class scope
298
303
  YAML
299
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
300
- expect(ClassMethod.say_class_defined).to eq('defined with explicit class scope')
304
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
305
+ expect(SingletonMethod.say_class_defined).to eq('defined with explicit class scope')
301
306
  end
302
307
  end
303
308
 
@@ -306,15 +311,15 @@ describe 'AppMap class Hooking' do
306
311
  ---
307
312
  - :id: 1
308
313
  :event: :call
309
- :defined_class: ClassMethod
314
+ :defined_class: SingletonMethod
310
315
  :method_id: say_self_defined
311
- :path: spec/fixtures/hook/class_method.rb
316
+ :path: spec/fixtures/hook/singleton_method.rb
312
317
  :lineno: 14
313
318
  :static: true
314
319
  :parameters: []
315
320
  :receiver:
316
321
  :class: Class
317
- :value: ClassMethod
322
+ :value: SingletonMethod
318
323
  - :id: 2
319
324
  :event: :return
320
325
  :parent_id: 1
@@ -322,8 +327,74 @@ describe 'AppMap class Hooking' do
322
327
  :class: String
323
328
  :value: defined with self class scope
324
329
  YAML
325
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
326
- expect(ClassMethod.say_self_defined).to eq('defined with self class scope')
330
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
331
+ expect(SingletonMethod.say_self_defined).to eq('defined with self class scope')
332
+ end
333
+ end
334
+
335
+
336
+ it 'hooks an included method' do
337
+ events_yaml = <<~YAML
338
+ ---
339
+ - :id: 1
340
+ :event: :call
341
+ :defined_class: SingletonMethod
342
+ :method_id: added_method
343
+ :path: spec/fixtures/hook/singleton_method.rb
344
+ :lineno: 44
345
+ :static: false
346
+ :parameters: []
347
+ :receiver:
348
+ :class: SingletonMethod
349
+ :value: Singleton Method fixture
350
+ - :id: 2
351
+ :event: :call
352
+ :defined_class: AddMethod
353
+ :method_id: _added_method
354
+ :path: spec/fixtures/hook/singleton_method.rb
355
+ :lineno: 50
356
+ :static: false
357
+ :parameters: []
358
+ :receiver:
359
+ :class: SingletonMethod
360
+ :value: Singleton Method fixture
361
+ - :id: 3
362
+ :event: :return
363
+ :parent_id: 2
364
+ :return_value:
365
+ :class: String
366
+ :value: defined by including a module
367
+ - :id: 4
368
+ :event: :return
369
+ :parent_id: 1
370
+ :return_value:
371
+ :class: String
372
+ :value: defined by including a module
373
+ YAML
374
+
375
+ load 'spec/fixtures/hook/singleton_method.rb'
376
+ setup = -> { SingletonMethod.new.do_include }
377
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
378
+ expect(s.added_method).to eq('defined by including a module')
379
+ end
380
+ end
381
+
382
+ it "doesn't hook a singleton method defined for an instance" do
383
+ # Ideally, Ruby would fire a TracePoint event when a singleton
384
+ # class gets created by defining a method on an instance. It
385
+ # currently doesn't, though, so there's no way for us to hook such
386
+ # a method.
387
+ #
388
+ # This example will fail if Ruby's behavior changes at some point
389
+ # in the future.
390
+ events_yaml = <<~YAML
391
+ --- []
392
+ YAML
393
+
394
+ load 'spec/fixtures/hook/singleton_method.rb'
395
+ setup = -> { SingletonMethod.new_with_instance_method }
396
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
397
+ expect(s.say_instance_defined).to eq('defined for an instance')
327
398
  end
328
399
  end
329
400
 
@@ -366,4 +437,108 @@ describe 'AppMap class Hooking' do
366
437
  expect { ExceptionMethod.new.raise_exception }.to raise_exception
367
438
  end
368
439
  end
440
+
441
+ context 'ActiveSupport::SecurityUtils.secure_compare' do
442
+ it 'is hooked' do
443
+ events_yaml = <<~YAML
444
+ ---
445
+ - :id: 1
446
+ :event: :call
447
+ :defined_class: Compare
448
+ :method_id: compare
449
+ :path: spec/fixtures/hook/compare.rb
450
+ :lineno: 4
451
+ :static: true
452
+ :parameters:
453
+ - :name: :s1
454
+ :class: String
455
+ :value: string
456
+ :kind: :req
457
+ - :name: :s2
458
+ :class: String
459
+ :value: string
460
+ :kind: :req
461
+ :receiver:
462
+ :class: Class
463
+ :value: Compare
464
+ - :id: 2
465
+ :event: :call
466
+ :defined_class: ActiveSupport::SecurityUtils
467
+ :method_id: secure_compare
468
+ :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
469
+ :lineno: 26
470
+ :static: true
471
+ :parameters:
472
+ - :name: :a
473
+ :class: String
474
+ :value: string
475
+ :kind: :req
476
+ - :name: :b
477
+ :class: String
478
+ :value: string
479
+ :kind: :req
480
+ :receiver:
481
+ :class: Module
482
+ :value: ActiveSupport::SecurityUtils
483
+ - :id: 3
484
+ :event: :return
485
+ :parent_id: 2
486
+ :return_value:
487
+ :class: TrueClass
488
+ :value: 'true'
489
+ - :id: 4
490
+ :event: :return
491
+ :parent_id: 1
492
+ :return_value:
493
+ :class: TrueClass
494
+ :value: 'true'
495
+ YAML
496
+
497
+ test_hook_behavior 'spec/fixtures/hook/compare.rb', events_yaml do
498
+ expect(Compare.compare('string', 'string')).to be_truthy
499
+ end
500
+ end
501
+
502
+ it 'gets labeled in the classmap' do
503
+ classmap_yaml = <<~YAML
504
+ ---
505
+ - :name: spec/fixtures/hook/compare.rb
506
+ :type: package
507
+ :children:
508
+ - :name: Compare
509
+ :type: class
510
+ :children:
511
+ - :name: compare
512
+ :type: function
513
+ :location: spec/fixtures/hook/compare.rb:4
514
+ :static: true
515
+ - :name: active_support
516
+ :type: package
517
+ :children:
518
+ - :name: ActiveSupport
519
+ :type: class
520
+ :children:
521
+ - :name: SecurityUtils
522
+ :type: class
523
+ :children:
524
+ - :name: secure_compare
525
+ :type: function
526
+ :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
527
+ :static: true
528
+ :labels:
529
+ - security
530
+ YAML
531
+
532
+ config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
533
+ expect(Compare.compare('string', 'string')).to be_truthy
534
+ end
535
+ cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
536
+ entry = cm[1][:children][0][:children][0][:children][0]
537
+ # Sanity check, make sure we got the right one
538
+ expect(entry[:name]).to eq('secure_compare')
539
+ spec = Gem::Specification.find_by_name('activesupport')
540
+ entry[:location].gsub!(spec.base_dir + '/', '')
541
+ expect(Diffy::Diff.new(cm.to_yaml, classmap_yaml).to_s).to eq('')
542
+ end
543
+ end
369
544
  end