diff options
20 files changed, 264 insertions, 34 deletions
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 35facd13c8..243fd40a7e 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -94,7 +94,6 @@ module ActionController extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } names.sort! - names end helpers.uniq! helpers diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index fb664a69dd..47a66fc194 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -34,6 +34,7 @@ module ActionController module Live class Buffer < ActionDispatch::Response::Buffer #:nodoc: def initialize(response) + @error_callback = nil super(response, SizedQueue.new(10)) end @@ -56,6 +57,14 @@ module ActionController super @buf.push nil end + + def on_error(&block) + @error_callback = block + end + + def call_on_error + @error_callback.call + end end class Response < ActionDispatch::Response #:nodoc: all @@ -121,6 +130,16 @@ module ActionController begin super(name) + rescue => e + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exceptionception + log_error(exceptionception) + ensure + log_error(e) + @_response.stream.close + end ensure @_response.commit! end @@ -129,6 +148,16 @@ module ActionController @_response.await_commit end + def log_error(exception) + logger = ActionController::Base.logger + return unless logger + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + logger.fatal("#{message}\n\n") + end + def response_body=(body) super response.stream.close if response diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b9a5e78fe9..a35a613158 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -18,7 +18,7 @@ module ActionController @_layouts = Hash.new(0) @_files = Hash.new(0) - ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:layout] if path @_layouts[path] += 1 @@ -28,7 +28,7 @@ module ActionController end end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ @@ -41,7 +41,7 @@ module ActionController @_templates[path] += 1 end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| next if payload[:virtual_path] # files don't have virtual path path = payload[:identifier] diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 912da741b7..049840e2e1 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -223,8 +223,8 @@ module Mime Mime.instance_eval { remove_const(symbol) } SET.delete_if { |v| v.eql?(mime) } - LOOKUP.delete_if { |k,v| v.eql?(mime) } - EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) } + LOOKUP.delete_if { |_,v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) } end end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 82c55660ea..a732e570f2 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -58,7 +58,7 @@ module ActionDispatch end end - parameterized_parts.keep_if { |_, v| v } + parameterized_parts.keep_if { |_, v| v } parameterized_parts end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 44290445d4..5d1740d0d4 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -18,7 +18,7 @@ module ActionDispatch def call(env) env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id - @app.call(env).tap { |status, headers, body| headers["X-Request-Id"] = env["action_dispatch.request_id"] } + @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] } end private diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index 550f4dbd0d..db219c8fa9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -13,7 +13,7 @@ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") def debug_hash(object) - object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end unless self.class.method_defined?(:debug_hash) %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index 9d947aea40..b181909bff 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -1,10 +1,8 @@ <% - traces = [ - ["Application Trace", @application_trace], - ["Framework Trace", @framework_trace], - ["Full Trace", @full_trace] - ] - names = traces.collect {|name, trace| name} + traces = { "Application Trace" => @application_trace, + "Framework Trace" => @framework_trace, + "Full Trace" => @full_trace } + names = traces.keys %> <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8c095d605a..80054c8a40 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -489,7 +489,7 @@ module ActionDispatch end options = app - app, path = options.find { |k, v| k.respond_to?(:call) } + app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end @@ -1361,7 +1361,7 @@ module ActionDispatch def match(path, *rest) if rest.empty? && Hash === path options = path - path, to = options.find { |name, value| name.is_a?(String) } + path, to = options.find { |name, _value| name.is_a?(String) } options[:to] = to options.delete(path) paths = [path] diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 07203428d4..d48a83e6c6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -665,7 +665,7 @@ module ActionDispatch end req = @request_class.new(env) - @router.recognize(req) do |route, matches, params| + @router.recognize(req) do |route, _matches, params| params.merge!(extras) params.each do |key, value| if value.is_a?(String) diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 9210bffd1d..496682e8bd 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -81,7 +81,7 @@ module ActionDispatch # Load routes.rb if it hasn't been loaded. generated_path, extra_keys = @routes.generate_extras(options, defaults) - found_extras = options.reject {|k, v| ! extra_keys.include? k} + found_extras = options.reject { |k, _| ! extra_keys.include? k } msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) @@ -120,7 +120,7 @@ module ActionDispatch options[:controller] = "/#{controller}" end - generate_options = options.dup.delete_if{ |k,v| defaults.key?(k) } + generate_options = options.dup.delete_if{ |k, _| defaults.key?(k) } assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index e481f3b245..3253a3d424 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -377,8 +377,8 @@ module ActionDispatch node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) } end - selected = elements.map do |_element| - text = _element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + selected = elements.map do |elem| + text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root css_select(root, "encoded:root", &block)[0] end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index e657283cec..630e6a9b78 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -6,7 +6,7 @@ module ActionDispatch module TestProcess def assigns(key = nil) assigns = {}.with_indifferent_access - @controller.view_assigns.each {|k, v| assigns.regular_writer(k, v)} + @controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) } key.nil? ? assigns : assigns[key] end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 36cfb7fca7..36dedf0676 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -1152,12 +1152,65 @@ module ActionView end end + # A +FormBuilder+ object is associated with a particular model object and + # allows you to generate fields associated with the model object. The + # +FormBuilder+ object is yielded when using +form_for+ or +fields_for+. + # For example: + # + # <%= form_for @person do |person_form| %> + # Name: <%= person_form.text_field :name %> + # Admin: <%= person_form.check_box :admin %> + # <% end %> + # + # In the above block, the a +FormBuilder+ object is yielded as the + # +person_form+ variable. This allows you to generate the +text_field+ + # and +check_box+ fields by specifying their eponymous methods, which + # modify the underlying template and associates the +@person+ model object + # with the form. + # + # The +FormBuilder+ object can be thought of as serving as a proxy for the + # methods in the +FormHelper+ module. This class, however, allows you to + # call methods with the model object you are building the form for. + # + # You can create your own custom FormBuilder templates by subclasses this + # class. For example: + # + # class MyFormBuilder < ActionView::Helpers::FormBuilder + # def div_radio_button(method, tag_value, options = {}) + # @template.content_tag(:div, + # @template.radio_button( + # @object_name, method, tag_value, objectify_options(options) + # ) + # ) + # end + # + # The above code creates a new method +div_radio_button+ which wraps a div + # around the a new radio button. Note that when options are passed in, you + # must called +objectify_options+ in order for the model object to get + # correctly passed to the method. If +objectify_options+ is not called, + # then the newly created helper will not be linked back to the model. + # + # The +div_radio_button+ code from above can now be used as follows: + # + # <%= form_for @person, :builder => MyFormBuilder do |f| %> + # I am a child: <%= f.div_radio_button(:admin, "child") %> + # I am an adult: <%= f.div_radio_button(:admin, "adult") %> + # <% end -%> + # + # The standard set of helper methods for form building are located in the + # +field_helpers+ class attribute. class FormBuilder include ModelNaming # The methods which wrap a form helper call. class_attribute :field_helpers - self.field_helpers = FormHelper.instance_methods - [:form_for, :convert_to_model, :model_name_from_record_or_class] + self.field_helpers = [:fields_for, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field] attr_accessor :object_name, :object, :options @@ -1239,7 +1292,7 @@ module ActionView # Admin? : <%= permission_fields.check_box :admin %> # <% end %> # - # <%= f.submit %> + # <%= person_form.submit %> # <% end %> # # In this case, the checkbox field will be represented by an HTML +input+ diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 5755444a65..34164a19f0 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -52,6 +52,29 @@ module ActionController def with_stale render :text => 'stale' if stale?(:etag => "123") end + + def exception_in_view + render 'doesntexist' + end + + def exception_with_callback + response.headers['Content-Type'] = 'text/event-stream' + + response.stream.on_error do + response.stream.write %(data: "500 Internal Server Error"\n\n) + response.stream.close + end + + raise 'An exception occurred...' + end + + def exception_in_exception_callback + response.headers['Content-Type'] = 'text/event-stream' + response.stream.on_error do + raise 'We need to go deeper.' + end + response.stream.write params[:widget][:didnt_check_for_nil] + end end tests TestController @@ -66,6 +89,21 @@ module ActionController TestResponse.new end + def assert_stream_closed + assert response.stream.closed?, 'stream should be closed' + end + + def capture_log_output + output = StringIO.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, ActiveSupport::Logger.new(output) + + begin + yield output + ensure + ActionController::Base.logger = old_logger + end + end + def test_set_response! @controller.set_response!(@request) assert_kind_of(Live::Response, @controller.response) @@ -119,7 +157,43 @@ module ActionController def test_render_text get :render_text assert_equal 'zomg', response.body - assert response.stream.closed?, 'stream should be closed' + assert_stream_closed + end + + def test_exception_handling_html + capture_log_output do |output| + get :exception_in_view + assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body + assert_match 'Missing template test/doesntexist', output.rewind && output.read + assert_stream_closed + end + end + + def test_exception_handling_plain_text + capture_log_output do |output| + get :exception_in_view, format: :json + assert_equal '', response.body + assert_match 'Missing template test/doesntexist', output.rewind && output.read + assert_stream_closed + end + end + + def test_exception_callback + capture_log_output do |output| + get :exception_with_callback, format: 'text/event-stream' + assert_equal %(data: "500 Internal Server Error"\n\n), response.body + assert_match 'An exception occurred...', output.rewind && output.read + assert_stream_closed + end + end + + def test_exceptions_raised_handling_exceptions + capture_log_output do |output| + get :exception_in_exception_callback, format: 'text/event-stream' + assert_equal '', response.body + assert_match 'We need to go deeper', output.rewind && output.read + assert_stream_closed + end end def test_stale_without_etag diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 93bbeea5a3..48febcbc43 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -373,7 +373,7 @@ namespace :railties do puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." end - on_copy = Proc.new do |name, migration, old_path| + on_copy = Proc.new do |name, migration| puts "Copied migration #{migration.basename} from #{name}" end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 886182f534..b631730ab3 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -9,11 +9,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - Thread.current["#{base_class}_current_scope"] + ScopeRegistry.current.value_for(:current_scope, base_class.to_s) end def current_scope=(scope) #:nodoc: - Thread.current["#{base_class}_current_scope"] = scope + ScopeRegistry.current.set_value_for(:current_scope, base_class.to_s, scope) end end @@ -24,5 +24,56 @@ module ActiveRecord send("#{att}=", value) if respond_to?("#{att}=") end end + + # This class stores the +:current_scope+ and +:ignore_default_scope+ values + # for different classes. The registry is stored as a thread local, which is + # accessed through +ScopeRegistry.current+. + # + # This class allows you to store and get the scope values on different + # classes and different types of scopes. For example, if you are attempting + # to get the current_scope for the +Board+ model, then you would use the + # following code: + # + # registry = ActiveRecord::Scoping::ScopeRegistry.current + # registry.set_value_for(:current_scope, "Board", some_new_scope) + # + # Now when you run: + # + # registry.value_for(:current_scope, "Board") + # + # You will obtain whatever was defined in +some_new_scope+. + class ScopeRegistry # :nodoc: + def self.current + Thread.current["scope_registry"] ||= new + end + + VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope] + + attr_accessor :registry + + def initialize + @registry = Hash.new { |hash, key| hash[key] = {} } + end + + # Obtains the value for a given +scope_name+ and +variable_name+. + def value_for(scope_type, variable_name) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] + end + + # Sets the +value+ for a given +scope_type+ and +variable_name+. + def set_value_for(scope_type, variable_name, value) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] = value + end + + private + + def raise_invalid_scope_type!(scope_type) + if !VALID_SCOPE_TYPES.include?(scope_type) + raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" + end + end + end end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 8a90528ce8..daee771669 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -120,11 +120,11 @@ module ActiveRecord end def ignore_default_scope? # :nodoc: - Thread.current["#{self}_ignore_default_scope"] + ScopeRegistry.current.value_for(:ignore_default_scope, self) end def ignore_default_scope=(ignore) # :nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore + ScopeRegistry.current.set_value_for(:ignore_default_scope, self, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index acf003bd80..e57dbd5a09 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1377,9 +1377,9 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil Thread.current[:UnloadablePost_current_scope] + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.current.value_for(:current_scope, "UnloadablePost") ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil Thread.current[:UnloadablePost_current_scope] + assert_nil ActiveRecord::Scoping::ScopeRegistry.current.value_for(:current_scope, "UnloadablePost") ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 705a4693b7..ab2bb5fafe 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -177,7 +177,33 @@ module ActiveSupport end def instrumenter - Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) + InstrumentationRegistry.instrumenter_for(notifier) + end + end + + # This class is a registry which holds all of the +Instrumenter+ objects + # in a particular thread local. To access the +Instrumenter+ object for a + # particular +notifier+, you can call the following method: + # + # InstrumentationRegistry.instrumenter_for(notifier) + # + # The instrumenters for multiple notifiers are held in a single instance of + # this class. + class InstrumentationRegistry # :nodoc: + class << self + delegate :instrumenter_for, to: :current + + def current + Thread.current[:instrumentation_registry] ||= new + end + end + + def initialize + @registry = {} + end + + def instrumenter_for(notifier) + @registry[notifier] ||= Instrumenter.new(notifier) end end |