jordan-brough-hoptoad_notifier 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG +161 -0
  2. data/INSTALL +25 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README.rdoc +384 -0
  5. data/Rakefile +217 -0
  6. data/SUPPORTED_RAILS_VERSIONS +9 -0
  7. data/TESTING.rdoc +8 -0
  8. data/generators/hoptoad/hoptoad_generator.rb +63 -0
  9. data/generators/hoptoad/lib/insert_commands.rb +34 -0
  10. data/generators/hoptoad/lib/rake_commands.rb +24 -0
  11. data/generators/hoptoad/templates/capistrano_hook.rb +6 -0
  12. data/generators/hoptoad/templates/hoptoad_notifier_tasks.rake +25 -0
  13. data/generators/hoptoad/templates/initializer.rb +6 -0
  14. data/lib/hoptoad_notifier.rb +148 -0
  15. data/lib/hoptoad_notifier/backtrace.rb +99 -0
  16. data/lib/hoptoad_notifier/capistrano.rb +20 -0
  17. data/lib/hoptoad_notifier/configuration.rb +232 -0
  18. data/lib/hoptoad_notifier/notice.rb +318 -0
  19. data/lib/hoptoad_notifier/rack.rb +40 -0
  20. data/lib/hoptoad_notifier/rails.rb +37 -0
  21. data/lib/hoptoad_notifier/rails/action_controller_catcher.rb +29 -0
  22. data/lib/hoptoad_notifier/rails/controller_methods.rb +63 -0
  23. data/lib/hoptoad_notifier/rails/error_lookup.rb +33 -0
  24. data/lib/hoptoad_notifier/rails3_tasks.rb +90 -0
  25. data/lib/hoptoad_notifier/railtie.rb +23 -0
  26. data/lib/hoptoad_notifier/sender.rb +63 -0
  27. data/lib/hoptoad_notifier/tasks.rb +97 -0
  28. data/lib/hoptoad_notifier/version.rb +3 -0
  29. data/lib/hoptoad_tasks.rb +44 -0
  30. data/lib/rails/generators/hoptoad/hoptoad_generator.rb +69 -0
  31. data/lib/templates/rescue.erb +91 -0
  32. data/rails/init.rb +1 -0
  33. data/script/integration_test.rb +38 -0
  34. data/test/backtrace_test.rb +118 -0
  35. data/test/catcher_test.rb +324 -0
  36. data/test/configuration_test.rb +208 -0
  37. data/test/helper.rb +239 -0
  38. data/test/hoptoad_tasks_test.rb +152 -0
  39. data/test/logger_test.rb +85 -0
  40. data/test/notice_test.rb +443 -0
  41. data/test/notifier_test.rb +222 -0
  42. data/test/rack_test.rb +58 -0
  43. data/test/rails_initializer_test.rb +36 -0
  44. data/test/sender_test.rb +123 -0
  45. metadata +205 -0
@@ -0,0 +1,217 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'cucumber/rake/task'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => [:test, :cucumber]
9
+
10
+ desc 'Test the hoptoad_notifier gem.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Run ginger tests'
18
+ task :ginger do
19
+ $LOAD_PATH << File.join(*%w[vendor ginger lib])
20
+ ARGV.clear
21
+ ARGV << 'test'
22
+ load File.join(*%w[vendor ginger bin ginger])
23
+ end
24
+
25
+ namespace :changeling do
26
+ desc "Bumps the version by a minor or patch version, depending on what was passed in."
27
+ task :bump, :part do |t, args|
28
+ # Thanks, Jeweler!
29
+ if HoptoadNotifier::VERSION =~ /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/
30
+ major = $1.to_i
31
+ minor = $2.to_i
32
+ patch = $3.to_i
33
+ build = $4
34
+ else
35
+ abort
36
+ end
37
+
38
+ case args[:part]
39
+ when /minor/
40
+ minor += 1
41
+ patch = 0
42
+ when /patch/
43
+ patch += 1
44
+ else
45
+ abort
46
+ end
47
+
48
+ version = [major, minor, patch, build].compact.join('.')
49
+
50
+ File.open(File.join("lib", "hoptoad_notifier", "version.rb"), "w") do |f|
51
+ f.write <<EOF
52
+ module HoptoadNotifier
53
+ VERSION = "#{version}".freeze
54
+ end
55
+ EOF
56
+ end
57
+ end
58
+
59
+ desc "Writes out the new CHANGELOG and prepares the release"
60
+ task :change do
61
+ load 'lib/hoptoad_notifier/version.rb'
62
+ file = "CHANGELOG"
63
+ old = File.read(file)
64
+ version = HoptoadNotifier::VERSION
65
+ message = "Bumping to version #{version}"
66
+
67
+ File.open(file, "w") do |f|
68
+ f.write <<EOF
69
+ Version #{version} - #{Date.today}
70
+ ===============================================================================
71
+
72
+ #{`git log $(git tag | tail -1)..HEAD | git shortlog`}
73
+ #{old}
74
+ EOF
75
+ end
76
+
77
+ exec ["#{ENV["EDITOR"]} #{file}",
78
+ "git commit -aqm '#{message}'",
79
+ "git tag -a -m '#{message}' v#{version}",
80
+ "echo '\n\n\033[32mMarked v#{version} /' `git show-ref -s refs/heads/master` 'for release. Run: rake changeling:push\033[0m\n\n'"].join(' && ')
81
+ end
82
+
83
+ desc "Bump by a minor version (1.2.3 => 1.3.0)"
84
+ task :minor do |t|
85
+ Rake::Task['changeling:bump'].invoke(t.name)
86
+ Rake::Task['changeling:change'].invoke
87
+ end
88
+
89
+ desc "Bump by a patch version, (1.2.3 => 1.2.4)"
90
+ task :patch do |t|
91
+ Rake::Task['changeling:bump'].invoke(t.name)
92
+ Rake::Task['changeling:change'].invoke
93
+ end
94
+
95
+ desc "Push the latest version and tags"
96
+ task :push do |t|
97
+ system("git push origin master")
98
+ system("git push origin $(git tag | tail -1)")
99
+ end
100
+ end
101
+
102
+ begin
103
+ require 'yard'
104
+ YARD::Rake::YardocTask.new do |t|
105
+ t.files = ['lib/**/*.rb', 'TESTING.rdoc']
106
+ end
107
+ rescue LoadError
108
+ end
109
+
110
+ GEM_ROOT = File.dirname(__FILE__).freeze
111
+ VERSION_FILE = File.join(GEM_ROOT, 'lib', 'hoptoad_notifier', 'version')
112
+
113
+ require VERSION_FILE
114
+
115
+ gemspec = Gem::Specification.new do |s|
116
+ s.name = %q{hoptoad_notifier}
117
+ s.version = HoptoadNotifier::VERSION
118
+ s.summary = %q{Send your application errors to our hosted service and reclaim your inbox.}
119
+
120
+ s.files = FileList['[A-Z]*', 'generators/**/*.*', 'lib/**/*.rb',
121
+ 'test/**/*.rb', 'rails/**/*.rb', 'script/*',
122
+ 'lib/templates/*.erb']
123
+ s.require_path = 'lib'
124
+ s.test_files = Dir[*['test/**/*_test.rb']]
125
+
126
+ s.has_rdoc = true
127
+ s.extra_rdoc_files = ["README.rdoc"]
128
+ s.rdoc_options = ['--line-numbers', "--main", "README.rdoc"]
129
+
130
+ s.add_runtime_dependency("activesupport")
131
+ s.add_development_dependency("activerecord")
132
+ s.add_development_dependency("actionpack")
133
+ s.add_development_dependency("jferris-mocha")
134
+ s.add_development_dependency("nokogiri")
135
+ s.add_development_dependency("shoulda")
136
+
137
+ s.authors = ["thoughtbot, inc"]
138
+ s.email = %q{support@hoptoadapp.com}
139
+ s.homepage = "http://www.hoptoadapp.com"
140
+
141
+ s.platform = Gem::Platform::RUBY
142
+ end
143
+
144
+ Rake::GemPackageTask.new gemspec do |pkg|
145
+ pkg.need_tar = true
146
+ pkg.need_zip = true
147
+ end
148
+
149
+ desc "Clean files generated by rake tasks"
150
+ task :clobber => [:clobber_rdoc, :clobber_package]
151
+
152
+ desc "Generate a gemspec file"
153
+ task :gemspec do
154
+ File.open("#{gemspec.name}.gemspec", 'w') do |f|
155
+ f.write gemspec.to_ruby
156
+ end
157
+ end
158
+
159
+ LOCAL_GEM_ROOT = File.join(GEM_ROOT, 'tmp', 'local_gems').freeze
160
+ RAILS_VERSIONS = IO.read('SUPPORTED_RAILS_VERSIONS').strip.split("\n")
161
+ LOCAL_GEMS = [['sham_rack', nil], ['capistrano', nil], ['sqlite3-ruby', nil], ['sinatra', nil]] +
162
+ RAILS_VERSIONS.collect { |version| ['rails', version] }
163
+
164
+ task :vendor_test_gems do
165
+ old_gem_path = ENV['GEM_PATH']
166
+ old_gem_home = ENV['GEM_HOME']
167
+ ENV['GEM_PATH'] = LOCAL_GEM_ROOT
168
+ ENV['GEM_HOME'] = LOCAL_GEM_ROOT
169
+ LOCAL_GEMS.each do |gem_name, version|
170
+ gem_file_pattern = [gem_name, version || '*'].compact.join('-')
171
+ version_option = version ? "-v #{version}" : ''
172
+ pattern = File.join(LOCAL_GEM_ROOT, 'gems', "#{gem_file_pattern}")
173
+ existing = Dir.glob(pattern).first
174
+ unless existing
175
+ command = "gem install -i #{LOCAL_GEM_ROOT} --no-ri --no-rdoc --backtrace #{version_option} #{gem_name}"
176
+ puts "Vendoring #{gem_file_pattern}..."
177
+ unless system("#{command} 2>&1")
178
+ puts "Command failed: #{command}"
179
+ exit(1)
180
+ end
181
+ end
182
+ end
183
+ ENV['GEM_PATH'] = old_gem_path
184
+ ENV['GEM_HOME'] = old_gem_home
185
+ end
186
+
187
+ Cucumber::Rake::Task.new(:cucumber) do |t|
188
+ t.fork = true
189
+ t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'progress')]
190
+ end
191
+
192
+ task :cucumber => [:gemspec, :vendor_test_gems]
193
+
194
+ def define_rails_cucumber_tasks(additional_cucumber_args = '')
195
+ namespace :rails do
196
+ RAILS_VERSIONS.each do |version|
197
+ desc "Test integration of the gem with Rails #{version}"
198
+ task version => [:gemspec, :vendor_test_gems] do
199
+ puts "Testing Rails #{version}"
200
+ ENV['RAILS_VERSION'] = version
201
+ system("cucumber --format #{ENV['CUCUMBER_FORMAT'] || 'progress'} #{additional_cucumber_args} features/rails.feature")
202
+ end
203
+ end
204
+
205
+ desc "Test integration of the gem with all Rails versions"
206
+ task :all => RAILS_VERSIONS
207
+ end
208
+ end
209
+
210
+ namespace :cucumber do
211
+ namespace :wip do
212
+ define_rails_cucumber_tasks('--tags @wip')
213
+ end
214
+
215
+ define_rails_cucumber_tasks
216
+ end
217
+
@@ -0,0 +1,9 @@
1
+ 1.2.6
2
+ 2.0.2
3
+ 2.1.0
4
+ 2.1.2
5
+ 2.2.2
6
+ 2.3.2
7
+ 2.3.4
8
+ 2.3.5
9
+ 3.0.0.beta4
@@ -0,0 +1,8 @@
1
+ = For Maintainers:
2
+
3
+ When developing the Hoptoad Notifier, be sure to use the integration test
4
+ against an existing project on staging before pushing to master.
5
+
6
+ +./script/integration_test.rb <test project's api key> <staging server hostname>+
7
+
8
+ +./script/integration_test.rb <test project's api key> <staging server hostname> secure+
@@ -0,0 +1,63 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/lib/insert_commands.rb")
2
+ require File.expand_path(File.dirname(__FILE__) + "/lib/rake_commands.rb")
3
+
4
+ class HoptoadGenerator < Rails::Generator::Base
5
+ def add_options!(opt)
6
+ opt.on('-k', '--api-key=key', String, "Your Hoptoad API key") {|v| options[:api_key] = v}
7
+ opt.on('-h', '--heroku', "Use the Heroku addon to provide your Hoptoad API key") {|v| options[:heroku] = v}
8
+ end
9
+
10
+ def manifest
11
+ if !api_key_configured? && !options[:api_key] && !options[:heroku]
12
+ puts "Must pass --api-key or --heroku or create config/initializers/hoptoad.rb"
13
+ exit
14
+ end
15
+ if plugin_is_present?
16
+ puts "You must first remove the hoptoad_notifier plugin. Please run: script/plugin remove hoptoad_notifier"
17
+ exit
18
+ end
19
+ record do |m|
20
+ m.directory 'lib/tasks'
21
+ m.file 'hoptoad_notifier_tasks.rake', 'lib/tasks/hoptoad_notifier_tasks.rake'
22
+ if ['config/deploy.rb', 'Capfile'].all? { |file| File.exists?(file) }
23
+ m.append_to 'config/deploy.rb', capistrano_hook
24
+ end
25
+ if api_key_expression
26
+ if use_initializer?
27
+ m.template 'initializer.rb', 'config/initializers/hoptoad.rb',
28
+ :assigns => {:api_key => api_key_expression}
29
+ else
30
+ m.template 'initializer.rb', 'config/hoptoad.rb',
31
+ :assigns => {:api_key => api_key_expression}
32
+ m.append_to 'config/environment.rb', "require 'config/hoptoad'"
33
+ end
34
+ end
35
+ m.rake "hoptoad:test", :generate_only => true
36
+ end
37
+ end
38
+
39
+ def api_key_expression
40
+ s = if options[:api_key]
41
+ "'#{options[:api_key]}'"
42
+ elsif options[:heroku]
43
+ "ENV['HOPTOAD_API_KEY']"
44
+ end
45
+ end
46
+
47
+ def use_initializer?
48
+ Rails::VERSION::MAJOR > 1
49
+ end
50
+
51
+ def api_key_configured?
52
+ File.exists?('config/initializers/hoptoad.rb') ||
53
+ system("grep HoptoadNotifier config/environment.rb")
54
+ end
55
+
56
+ def capistrano_hook
57
+ IO.read(source_path('capistrano_hook.rb'))
58
+ end
59
+
60
+ def plugin_is_present?
61
+ File.exists?('vendor/plugins/hoptoad_notifier')
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ # Mostly pinched from http://github.com/ryanb/nifty-generators/tree/master
2
+
3
+ Rails::Generator::Commands::Base.class_eval do
4
+ def file_contains?(relative_destination, line)
5
+ File.read(destination_path(relative_destination)).include?(line)
6
+ end
7
+ end
8
+
9
+ Rails::Generator::Commands::Create.class_eval do
10
+ def append_to(file, line)
11
+ logger.insert "#{line} appended to #{file}"
12
+ unless options[:pretend] || file_contains?(file, line)
13
+ File.open(file, "a") do |file|
14
+ file.puts
15
+ file.puts line
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Rails::Generator::Commands::Destroy.class_eval do
22
+ def append_to(file, line)
23
+ logger.remove "#{line} removed from #{file}"
24
+ unless options[:pretend]
25
+ gsub_file file, "\n#{line}", ''
26
+ end
27
+ end
28
+ end
29
+
30
+ Rails::Generator::Commands::List.class_eval do
31
+ def append_to(file, line)
32
+ logger.insert "#{line} appended to #{file}"
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ Rails::Generator::Commands::Create.class_eval do
2
+ def rake(cmd, opts = {})
3
+ logger.rake "rake #{cmd}"
4
+ unless system("rake #{cmd}")
5
+ logger.rake "#{cmd} failed. Rolling back"
6
+ command(:destroy).invoke!
7
+ end
8
+ end
9
+ end
10
+
11
+ Rails::Generator::Commands::Destroy.class_eval do
12
+ def rake(cmd, opts = {})
13
+ unless opts[:generate_only]
14
+ logger.rake "rake #{cmd}"
15
+ system "rake #{cmd}"
16
+ end
17
+ end
18
+ end
19
+
20
+ Rails::Generator::Commands::List.class_eval do
21
+ def rake(cmd, opts = {})
22
+ logger.rake "rake #{cmd}"
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+
2
+ Dir[File.join(File.dirname(__FILE__), '..', 'vendor', 'gems', 'hoptoad_notifier-*')].each do |vendored_notifier|
3
+ $: << File.join(vendored_notifier, 'lib')
4
+ end
5
+
6
+ require 'hoptoad_notifier/capistrano'
@@ -0,0 +1,25 @@
1
+ # Don't load anything when running the gems:* tasks.
2
+ # Otherwise, hoptoad_notifier will be considered a framework gem.
3
+ # https://thoughtbot.lighthouseapp.com/projects/14221/tickets/629
4
+ unless ARGV.any? {|a| a =~ /^gems/}
5
+
6
+ Dir[File.join(RAILS_ROOT, 'vendor', 'gems', 'hoptoad_notifier-*')].each do |vendored_notifier|
7
+ $: << File.join(vendored_notifier, 'lib')
8
+ end
9
+
10
+ begin
11
+ require 'hoptoad_notifier/tasks'
12
+ rescue LoadError => exception
13
+ namespace :hoptoad do
14
+ %w(deploy test log_stdout).each do |task_name|
15
+ desc "Missing dependency for hoptoad:#{task_name}"
16
+ task task_name do
17
+ $stderr.puts "Failed to run hoptoad:#{task_name} because of missing dependency."
18
+ $stderr.puts "You probably need to run `rake gems:install` to install the hoptoad_notifier gem"
19
+ abort exception.inspect
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,6 @@
1
+ <% if Rails::VERSION::MAJOR < 3 && Rails::VERSION::MINOR < 2 -%>
2
+ require 'hoptoad_notifier/rails'
3
+ <% end -%>
4
+ HoptoadNotifier.configure do |config|
5
+ config.api_key = <%= api_key_expression %>
6
+ end
@@ -0,0 +1,148 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'rubygems'
4
+ require 'active_support'
5
+ require 'hoptoad_notifier/version'
6
+ require 'hoptoad_notifier/configuration'
7
+ require 'hoptoad_notifier/notice'
8
+ require 'hoptoad_notifier/sender'
9
+ require 'hoptoad_notifier/backtrace'
10
+ require 'hoptoad_notifier/rack'
11
+
12
+ require 'hoptoad_notifier/railtie' if defined?(Rails::Railtie)
13
+
14
+ # Gem for applications to automatically post errors to the Hoptoad of their choice.
15
+ module HoptoadNotifier
16
+
17
+ API_VERSION = "2.0"
18
+ LOG_PREFIX = "** [Hoptoad] "
19
+
20
+ HEADERS = {
21
+ 'Content-type' => 'text/xml',
22
+ 'Accept' => 'text/xml, application/xml'
23
+ }
24
+
25
+ class << self
26
+ # The sender object is responsible for delivering formatted data to the Hoptoad server.
27
+ # Must respond to #send_to_hoptoad. See HoptoadNotifier::Sender.
28
+ attr_accessor :sender
29
+
30
+ # A Hoptoad configuration object. Must act like a hash and return sensible
31
+ # values for all Hoptoad configuration options. See HoptoadNotifier::Configuration.
32
+ attr_accessor :configuration
33
+
34
+ # Tell the log that the Notifier is good to go
35
+ def report_ready
36
+ write_verbose_log("Notifier #{VERSION} ready to catch errors")
37
+ end
38
+
39
+ # Prints out the environment info to the log for debugging help
40
+ def report_environment_info
41
+ write_verbose_log("Environment Info: #{environment_info}")
42
+ end
43
+
44
+ # Prints out the response body from Hoptoad for debugging help
45
+ def report_response_body(response)
46
+ write_verbose_log("Response from Hoptoad: \n#{response}")
47
+ end
48
+
49
+ # Returns the Ruby version, Rails version, and current Rails environment
50
+ def environment_info
51
+ info = "[Ruby: #{RUBY_VERSION}]"
52
+ info << " [#{configuration.framework}]"
53
+ info << " [Env: #{configuration.environment_name}]"
54
+ end
55
+
56
+ # Writes out the given message to the #logger
57
+ def write_verbose_log(message)
58
+ logger.info LOG_PREFIX + message if logger
59
+ end
60
+
61
+ # Look for the Rails logger currently defined
62
+ def logger
63
+ self.configuration.logger
64
+ end
65
+
66
+ # Call this method to modify defaults in your initializers.
67
+ #
68
+ # @example
69
+ # HoptoadNotifier.configure do |config|
70
+ # config.api_key = '1234567890abcdef'
71
+ # config.secure = false
72
+ # end
73
+ def configure(silent = false)
74
+ self.configuration ||= Configuration.new
75
+ yield(configuration)
76
+ self.sender = Sender.new(configuration)
77
+ report_ready unless silent
78
+ end
79
+
80
+ # Sends an exception manually using this method, even when you are not in a controller.
81
+ #
82
+ # @param [Exception] exception The exception you want to notify Hoptoad about.
83
+ # @param [Hash] opts Data that will be sent to Hoptoad.
84
+ #
85
+ # @option opts [String] :api_key The API key for this project. The API key is a unique identifier that Hoptoad uses for identification.
86
+ # @option opts [String] :error_message The error returned by the exception (or the message you want to log).
87
+ # @option opts [String] :backtrace A backtrace, usually obtained with +caller+.
88
+ # @option opts [String] :request The controller's request object.
89
+ # @option opts [String] :session The contents of the user's session.
90
+ # @option opts [String] :environment ENV merged with the contents of the request's environment.
91
+ def notify(exception, opts = {})
92
+ send_notice(build_notice_for(exception, opts))
93
+ end
94
+
95
+ # Sends the notice unless it is one of the default ignored exceptions
96
+ # @see HoptoadNotifier.notify
97
+ def notify_or_ignore(exception, opts = {})
98
+ notice = build_notice_for(exception, opts)
99
+ send_notice(notice) unless notice.ignore?
100
+ end
101
+
102
+ def build_lookup_hash_for(exception, options = {})
103
+ notice = build_notice_for(exception, options)
104
+
105
+ result = {}
106
+ result[:action] = notice.action rescue nil
107
+ result[:component] = notice.component rescue nil
108
+ result[:error_class] = notice.error_class if notice.error_class
109
+ result[:environment_name] = 'production'
110
+
111
+ unless notice.backtrace.lines.empty?
112
+ result[:file] = notice.backtrace.lines.first.file
113
+ result[:line_number] = notice.backtrace.lines.first.number
114
+ end
115
+
116
+ result
117
+ end
118
+
119
+ private
120
+
121
+ def send_notice(notice)
122
+ if configuration.public?
123
+ sender.send_to_hoptoad(notice.to_xml)
124
+ end
125
+ end
126
+
127
+ def build_notice_for(exception, opts = {})
128
+ exception = unwrap_exception(exception)
129
+ if exception.respond_to?(:to_hash)
130
+ opts = opts.merge(exception)
131
+ else
132
+ opts = opts.merge(:exception => exception)
133
+ end
134
+ Notice.new(configuration.merge(opts))
135
+ end
136
+
137
+ def unwrap_exception(exception)
138
+ if exception.respond_to?(:original_exception)
139
+ exception.original_exception
140
+ elsif exception.respond_to?(:continued_exception)
141
+ exception.continued_exception
142
+ else
143
+ exception
144
+ end
145
+ end
146
+ end
147
+ end
148
+