bullet 5.9.0 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +27 -0
  4. data/Gemfile.rails-4.0 +1 -1
  5. data/Gemfile.rails-4.1 +1 -1
  6. data/Gemfile.rails-4.2 +1 -1
  7. data/Gemfile.rails-5.0 +1 -1
  8. data/Gemfile.rails-5.1 +1 -1
  9. data/Gemfile.rails-5.2 +1 -1
  10. data/Gemfile.rails-6.0 +15 -0
  11. data/Gemfile.rails-6.1 +15 -0
  12. data/README.md +31 -10
  13. data/lib/bullet.rb +50 -26
  14. data/lib/bullet/active_job.rb +13 -0
  15. data/lib/bullet/active_record4.rb +9 -32
  16. data/lib/bullet/active_record41.rb +7 -27
  17. data/lib/bullet/active_record42.rb +8 -24
  18. data/lib/bullet/active_record5.rb +188 -179
  19. data/lib/bullet/active_record52.rb +176 -168
  20. data/lib/bullet/active_record60.rb +267 -0
  21. data/lib/bullet/active_record61.rb +267 -0
  22. data/lib/bullet/bullet_xhr.js +63 -0
  23. data/lib/bullet/dependency.rb +50 -36
  24. data/lib/bullet/detector/association.rb +26 -20
  25. data/lib/bullet/detector/base.rb +1 -2
  26. data/lib/bullet/detector/counter_cache.rb +13 -9
  27. data/lib/bullet/detector/n_plus_one_query.rb +22 -12
  28. data/lib/bullet/detector/unused_eager_loading.rb +6 -3
  29. data/lib/bullet/ext/object.rb +4 -2
  30. data/lib/bullet/mongoid4x.rb +2 -6
  31. data/lib/bullet/mongoid5x.rb +2 -6
  32. data/lib/bullet/mongoid6x.rb +2 -6
  33. data/lib/bullet/mongoid7x.rb +2 -6
  34. data/lib/bullet/notification/base.rb +14 -18
  35. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  36. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  37. data/lib/bullet/rack.rb +50 -25
  38. data/lib/bullet/stack_trace_filter.rb +6 -12
  39. data/lib/bullet/version.rb +1 -1
  40. data/lib/generators/bullet/install_generator.rb +23 -23
  41. data/perf/benchmark.rb +8 -14
  42. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  43. data/spec/bullet/detector/n_plus_one_query_spec.rb +7 -3
  44. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  45. data/spec/bullet/ext/object_spec.rb +9 -4
  46. data/spec/bullet/notification/base_spec.rb +1 -3
  47. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  48. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  49. data/spec/bullet/rack_spec.rb +86 -6
  50. data/spec/bullet/registry/association_spec.rb +2 -2
  51. data/spec/bullet/registry/base_spec.rb +1 -1
  52. data/spec/bullet_spec.rb +11 -30
  53. data/spec/integration/active_record/association_spec.rb +44 -136
  54. data/spec/integration/counter_cache_spec.rb +11 -31
  55. data/spec/integration/mongoid/association_spec.rb +18 -32
  56. data/spec/models/folder.rb +1 -2
  57. data/spec/models/group.rb +1 -2
  58. data/spec/models/page.rb +1 -2
  59. data/spec/models/writer.rb +1 -2
  60. data/spec/spec_helper.rb +6 -10
  61. data/spec/support/bullet_ext.rb +8 -9
  62. data/spec/support/mongo_seed.rb +2 -16
  63. data/test.sh +1 -0
  64. metadata +12 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fefb038c3c46f100cc22cdd95c417c781aff2943
4
- data.tar.gz: ad23b5755c4956788bad2b3ae98323d6e24c2bb8
2
+ SHA256:
3
+ metadata.gz: 6d02240600f13580ed16200edd5c39563bec1cb49a46ccbc82c57cff73a46e2f
4
+ data.tar.gz: 0f40a4a1fe6157dabec88cb0ecc817168eda794aa1177c41ede1df351695d76a
5
5
  SHA512:
6
- metadata.gz: 79f4e0bb1d4e62df6460fd668f0789936e3e0b1c36d078bb13ec2788f9d69f46e0dd3615e1983019bc4c83d6e7d2b2195377c5216a23038d270aee2ddfb6e77c
7
- data.tar.gz: 504905ca503f0727e71c1c476ab058eab57618bc6a4a9084411c479ebd63913c75ac178cf27566d05e800887e1d6f1e709aa89733632743c7216db810d410fb8
6
+ metadata.gz: 1d3d3ce81767ce81091ddd7412dd8f341190ffac085477cde3ea04fa1ed696661f84be64d9c1fb6e6a0de276788e56f18aaada213df712cb51e6efb0a577ec37
7
+ data.tar.gz: a3633af514a31e0ad6ff317a98616ed2c6c3ce070109288b8514c80277d4098359ed25a60bd5bcc4cd0ebf469f156751fa707f8d410f010bfcb8828e206e08a3
@@ -1,12 +1,33 @@
1
- sudo: false
2
1
  language: ruby
3
2
  rvm:
4
3
  - 2.3.0
4
+ - 2.6.0
5
5
  gemfile:
6
+ - Gemfile.rails-6.0
7
+ - Gemfile.rails-5.2
6
8
  - Gemfile.rails-5.1
7
9
  - Gemfile.rails-5.0
8
10
  - Gemfile.rails-4.2
9
11
  - Gemfile.rails-4.1
10
12
  - Gemfile.rails-4.0
13
+ matrix:
14
+ exclude:
15
+ - rvm: 2.3.0
16
+ gemfile: Gemfile.rails-6.0
17
+ - rvm: 2.6.0
18
+ gemfile: Gemfile.rails-5.2
19
+ - rvm: 2.6.0
20
+ gemfile: Gemfile.rails-5.1
21
+ - rvm: 2.6.0
22
+ gemfile: Gemfile.rails-5.0
23
+ - rvm: 2.6.0
24
+ gemfile: Gemfile.rails-4.2
25
+ - rvm: 2.6.0
26
+ gemfile: Gemfile.rails-4.1
27
+ - rvm: 2.6.0
28
+ gemfile: Gemfile.rails-4.0
11
29
  env:
12
30
  - DB=sqlite
31
+ before_install:
32
+ - "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
33
+ - gem install bundler -v '< 2'
@@ -1,5 +1,32 @@
1
1
  ## Next Release
2
2
 
3
+ ## 6.1.1 (12/12/2020)
4
+
5
+ * Add support Rails 6.1
6
+ * Make whitelist thread safe
7
+
8
+ ## 6.1.0 (12/28/2019)
9
+
10
+ * Add skip_html_injection flag
11
+ * Remove writer hack in active_record6
12
+ * Use modern includes syntax in warnings
13
+ * Fix warning: The last argument is used as the keyword parameter
14
+
15
+ ## 6.0.2 (08/20/2019)
16
+
17
+ * Fully support Rails 6.0
18
+
19
+ ## 6.0.1 (06/26/2019)
20
+
21
+ * Add Bullet::ActiveJob
22
+ * Prevent "Maximum call stack exceeded" errors when used with Turbolinks
23
+
24
+ ## 6.0.0 (04/25/2019)
25
+
26
+ * Add XHR support to Bullet
27
+ * Support Rails 6.0
28
+ * Handle case where ID is manually set on unpersisted record
29
+
3
30
  ## 5.9.0 (11/11/2018)
4
31
 
5
32
  * Require Ruby 2.3+
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 4.0.0'
6
- gem 'sqlite3', platforms: [:ruby]
6
+ gem 'sqlite3', '~> 1.3.6', platforms: [:ruby]
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
  gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 4.1.0'
6
- gem 'sqlite3'
6
+ gem 'sqlite3', '~> 1.3.6'
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
  gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 4.2.0'
6
- gem 'sqlite3'
6
+ gem 'sqlite3', '~> 1.3.6'
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
  gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 5.0.0'
6
- gem 'sqlite3'
6
+ gem 'sqlite3', '~> 1.3.6'
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
 
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 5.1.0'
6
- gem 'sqlite3'
6
+ gem 'sqlite3', '~> 1.3.6'
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
 
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rails', '~> 5.2.0'
6
- gem 'sqlite3'
6
+ gem 'sqlite3', '~> 1.3.6'
7
7
  gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
8
  gem 'activerecord-import'
9
9
 
@@ -0,0 +1,15 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rails', '~> 6.0.0'
6
+ gem 'sqlite3'
7
+ gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
+ gem 'activerecord-import'
9
+
10
+ gem "rspec"
11
+
12
+ platforms :rbx do
13
+ gem 'rubysl', '~> 2.0'
14
+ gem 'rubinius-developer_tools'
15
+ end
@@ -0,0 +1,15 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rails', '~> 6.1.0'
6
+ gem 'sqlite3'
7
+ gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
8
+ gem 'activerecord-import'
9
+
10
+ gem "rspec"
11
+
12
+ platforms :rbx do
13
+ gem 'rubysl', '~> 2.0'
14
+ gem 'rubinius-developer_tools'
15
+ end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Bullet
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/bullet.png)](http://badge.fury.io/rb/bullet)
4
- [![Build Status](https://secure.travis-ci.org/flyerhzm/bullet.png)](http://travis-ci.org/flyerhzm/bullet)
3
+ [![Gem Version](https://badge.fury.io/rb/bullet.svg)](http://badge.fury.io/rb/bullet)
4
+ [![Build Status](https://secure.travis-ci.org/flyerhzm/bullet.svg)](http://travis-ci.org/flyerhzm/bullet)
5
5
  [![AwesomeCode Status for flyerhzm/bullet](https://awesomecode.io/projects/6755235b-e2c1-459e-bf92-b8b13d0c0472/status)](https://awesomecode.io/repos/flyerhzm/bullet)
6
6
  [![Coderwall Endorse](http://api.coderwall.com/flyerhzm/endorsecount.png)](http://coderwall.com/flyerhzm)
7
7
 
@@ -37,6 +37,13 @@ or add it into a Gemfile (Bundler):
37
37
  gem 'bullet', group: 'development'
38
38
  ```
39
39
 
40
+ enable the Bullet gem with generate command
41
+
42
+ ```ruby
43
+ bundle exec rails g bullet:install
44
+ ```
45
+ The generate command will auto generate the default configuration and may ask to include in the test environment as well. See below for custom configuration.
46
+
40
47
  **Note**: make sure `bullet` gem is added after activerecord (rails) and
41
48
  mongoid.
42
49
 
@@ -63,6 +70,7 @@ config.after_initialize do
63
70
  Bullet.airbrake = true
64
71
  Bullet.rollbar = true
65
72
  Bullet.add_footer = true
73
+ Bullet.skip_html_injection = false
66
74
  Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
67
75
  Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
68
76
  Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
@@ -85,6 +93,7 @@ The code above will enable all of the Bullet notification systems:
85
93
  * `Bullet.rollbar`: add notifications to rollbar
86
94
  * `Bullet.sentry`: add notifications to sentry
87
95
  * `Bullet.add_footer`: adds the details in the bottom left corner of the page. Double click the footer or use close button to hide footer.
96
+ * `Bullet.skip_html_injection`: prevents Bullet from injecting XHR into the returned HTML. This must be false for receiving alerts or console logging.
88
97
  * `Bullet.stacktrace_includes`: include paths with any of these substrings in the stack trace, even if they are not in your main app
89
98
  * `Bullet.stacktrace_excludes`: ignore paths with any of these substrings in the stack trace, even if they are not in your main app.
90
99
  Each item can be a string (match substring), a regex, or an array where the first item is a path to match, and the second
@@ -125,7 +134,7 @@ do like
125
134
 
126
135
  ```ruby
127
136
  class ApplicationController < ActionController::Base
128
- around_action :skip_bullet
137
+ around_action :skip_bullet, if: -> { defined?(Bullet) }
129
138
 
130
139
  def skip_bullet
131
140
  previous_value = Bullet.enable?
@@ -181,15 +190,27 @@ If you find Bullet does not work for you, *please disable your browser's cache*.
181
190
 
182
191
  ## Advanced
183
192
 
184
- ### Profile a job
193
+ ### Work with ActiveJob
185
194
 
186
- The Bullet gem uses rack middleware to profile requests. If you want to use Bullet without an http server, like to profile a job, you can use the profile method and fetch warnings
195
+ Include `Bullet::ActiveJob` in your `ApplicationJob`.
187
196
 
188
197
  ```ruby
189
- Bullet.profile do
190
- # do anything
198
+ class ApplicationJob < ActiveJob::Base
199
+ include Bullet::ActiveJob if Rails.env.development?
200
+ end
201
+ ```
202
+
203
+ ### Work with other background job solution
204
+
205
+ Use the Bullet.profile method.
191
206
 
192
- warnings = Bullet.warnings
207
+ ```ruby
208
+ class ApplicationJob < ActiveJob::Base
209
+ around_perform do |_job, block|
210
+ Bullet.profile do
211
+ block.call
212
+ end
213
+ end
193
214
  end
194
215
  ```
195
216
 
@@ -221,7 +242,7 @@ end
221
242
  Then wrap each test in Bullet api.
222
243
 
223
244
  ```ruby
224
- # spec/spec_helper.rb
245
+ # spec/rails_helper.rb
225
246
  if Bullet.enable?
226
247
  config.before(:each) do
227
248
  Bullet.start_request
@@ -458,4 +479,4 @@ Meanwhile, there's a line appended to `log/bullet.log`
458
479
  Post => [:comments]
459
480
  ```
460
481
 
461
- Copyright (c) 2009 - 2016 Richard Huang (flyerhzm@gmail.com), released under the MIT license
482
+ Copyright (c) 2009 - 2019 Richard Huang (flyerhzm@gmail.com), released under the MIT license
@@ -14,6 +14,7 @@ module Bullet
14
14
  autoload :ActiveRecord, "bullet/#{active_record_version}" if active_record?
15
15
  autoload :Mongoid, "bullet/#{mongoid_version}" if mongoid?
16
16
  autoload :Rack, 'bullet/rack'
17
+ autoload :ActiveJob, 'bullet/active_job'
17
18
  autoload :Notification, 'bullet/notification'
18
19
  autoload :Detector, 'bullet/detector'
19
20
  autoload :Registry, 'bullet/registry'
@@ -22,7 +23,7 @@ module Bullet
22
23
  BULLET_DEBUG = 'BULLET_DEBUG'
23
24
  TRUE = 'true'
24
25
 
25
- if defined? Rails::Railtie
26
+ if defined?(Rails::Railtie)
26
27
  class BulletRailtie < Rails::Railtie
27
28
  initializer 'bullet.configure_rails_initialization' do |app|
28
29
  app.middleware.use Bullet::Rack
@@ -31,28 +32,35 @@ module Bullet
31
32
  end
32
33
 
33
34
  class << self
34
- attr_writer :n_plus_one_query_enable, :unused_eager_loading_enable, :counter_cache_enable, :stacktrace_includes, :stacktrace_excludes
35
- attr_reader :whitelist
36
- attr_accessor :add_footer, :orm_pathches_applied
35
+ attr_writer :n_plus_one_query_enable,
36
+ :unused_eager_loading_enable,
37
+ :counter_cache_enable,
38
+ :stacktrace_includes,
39
+ :stacktrace_excludes,
40
+ :skip_html_injection
41
+ attr_accessor :add_footer, :orm_patches_applied
37
42
 
38
43
  available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.map { |notifier| "#{notifier}=" }
39
- available_notifiers << { to: UniformNotifier }
40
- delegate(*available_notifiers)
44
+ available_notifiers_options = { to: UniformNotifier }
45
+ delegate(*available_notifiers, **available_notifiers_options)
41
46
 
42
47
  def raise=(should_raise)
43
48
  UniformNotifier.raise = (should_raise ? Notification::UnoptimizedQueryError : false)
44
49
  end
45
50
 
46
- DETECTORS = [Bullet::Detector::NPlusOneQuery,
47
- Bullet::Detector::UnusedEagerLoading,
48
- Bullet::Detector::CounterCache].freeze
51
+ DETECTORS = [
52
+ Bullet::Detector::NPlusOneQuery,
53
+ Bullet::Detector::UnusedEagerLoading,
54
+ Bullet::Detector::CounterCache
55
+ ].freeze
49
56
 
50
57
  def enable=(enable)
51
58
  @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
59
+
52
60
  if enable?
53
61
  reset_whitelist
54
- unless orm_pathches_applied
55
- self.orm_pathches_applied = true
62
+ unless orm_patches_applied
63
+ self.orm_patches_applied = true
56
64
  Bullet::Mongoid.enable if mongoid?
57
65
  Bullet::ActiveRecord.enable if active_record?
58
66
  end
@@ -63,6 +71,10 @@ module Bullet
63
71
  !!@enable
64
72
  end
65
73
 
74
+ def app_root
75
+ (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
76
+ end
77
+
66
78
  def n_plus_one_query_enable?
67
79
  enable? && !!@n_plus_one_query_enable
68
80
  end
@@ -85,35 +97,34 @@ module Bullet
85
97
 
86
98
  def add_whitelist(options)
87
99
  reset_whitelist
88
- @whitelist[options[:type]][options[:class_name]] ||= []
89
- @whitelist[options[:type]][options[:class_name]] << options[:association].to_sym
100
+ Thread.current[:whitelist][options[:type]][options[:class_name]] ||= []
101
+ Thread.current[:whitelist][options[:type]][options[:class_name]] << options[:association].to_sym
90
102
  end
91
103
 
92
104
  def delete_whitelist(options)
93
105
  reset_whitelist
94
- @whitelist[options[:type]][options[:class_name]] ||= []
95
- @whitelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
96
- @whitelist[options[:type]].delete_if { |_key, val| val.empty? }
106
+ Thread.current[:whitelist][options[:type]][options[:class_name]] ||= []
107
+ Thread.current[:whitelist][options[:type]][options[:class_name]].delete(options[:association].to_sym)
108
+ Thread.current[:whitelist][options[:type]].delete_if { |_key, val| val.empty? }
97
109
  end
98
110
 
99
111
  def get_whitelist_associations(type, class_name)
100
- Array(@whitelist[type][class_name])
112
+ Array(Thread.current[:whitelist][type][class_name])
101
113
  end
102
114
 
103
115
  def reset_whitelist
104
- @whitelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
116
+ Thread.current[:whitelist] ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
105
117
  end
106
118
 
107
119
  def clear_whitelist
108
- @whitelist = nil
120
+ Thread.current[:whitelist] = nil
109
121
  end
110
122
 
111
123
  def bullet_logger=(active)
112
124
  if active
113
125
  require 'fileutils'
114
- root_path = (rails? ? Rails.root.to_s : Dir.pwd).to_s
115
- FileUtils.mkdir_p(root_path + '/log')
116
- bullet_log_file = File.open("#{root_path}/log/bullet.log", 'a+')
126
+ FileUtils.mkdir_p(app_root + '/log')
127
+ bullet_log_file = File.open("#{app_root}/log/bullet.log", 'a+')
117
128
  bullet_log_file.sync = true
118
129
  UniformNotifier.customized_logger = bullet_log_file
119
130
  end
@@ -170,9 +181,7 @@ module Bullet
170
181
 
171
182
  def gather_inline_notifications
172
183
  responses = []
173
- for_each_active_notifier_with_notification do |notification|
174
- responses << notification.notify_inline
175
- end
184
+ for_each_active_notifier_with_notification { |notification| responses << notification.notify_inline }
176
185
  responses.join("\n")
177
186
  end
178
187
 
@@ -185,9 +194,15 @@ module Bullet
185
194
  end
186
195
 
187
196
  def footer_info
197
+ info = []
198
+ notification_collector.collection.each { |notification| info << notification.short_notice }
199
+ info
200
+ end
201
+
202
+ def text_notifications
188
203
  info = []
189
204
  notification_collector.collection.each do |notification|
190
- info << notification.short_notice
205
+ info << notification.notification_data.values.compact.join("\n")
191
206
  end
192
207
  info
193
208
  end
@@ -202,6 +217,7 @@ module Bullet
202
217
 
203
218
  def profile
204
219
  return_value = nil
220
+
205
221
  if Bullet.enable?
206
222
  begin
207
223
  Bullet.start_request
@@ -219,6 +235,14 @@ module Bullet
219
235
  return_value
220
236
  end
221
237
 
238
+ def console_enabled?
239
+ UniformNotifier.active_notifiers.include?(UniformNotifier::JavascriptConsole)
240
+ end
241
+
242
+ def inject_into_page?
243
+ !@skip_html_injection && (console_enabled? || add_footer)
244
+ end
245
+
222
246
  private
223
247
 
224
248
  def for_each_active_notifier_with_notification
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module ActiveJob
5
+ def self.included(base)
6
+ base.class_eval do
7
+ around_perform do |_job, block|
8
+ Bullet.profile { block.call }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end