diff options
43 files changed, 575 insertions, 352 deletions
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 9a7da54c95..43c481339a 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -10,27 +10,45 @@ module ActionController DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' - def initialize(env = {}) - super + def self.new_session + TestSession.new + end + + # Create a new test request with default `env` values + def self.create + env = {} + env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + env["rack.request.cookie_hash"] = {}.with_indifferent_access + new(default_env.merge(env), new_session) + end + + def self.default_env + DEFAULT_ENV + end + private_class_method :default_env - self.session = TestSession.new + def initialize(env, session) + super(env) + + self.session = session self.session_options = TestSession::DEFAULT_OPTIONS end + def query_string=(string) + @env[Rack::QUERY_STRING] = string + end + + def request_parameters=(params) + @env["action_dispatch.request.request_parameters"] = params + end + def assign_parameters(routes, controller_path, action, parameters = {}) parameters = parameters.symbolize_keys - extra_keys = routes.extra_keys(parameters.merge(:controller => controller_path, :action => action)) - non_path_parameters = get? ? query_parameters : request_parameters + generated_path, extra_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action)) + non_path_parameters = {} + path_parameters = {} parameters.each do |key, value| - if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) - value = value.map{ |v| v.duplicable? ? v.dup : v } - elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) - value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] - elsif value.frozen? && value.duplicable? - value = value.dup - end - if extra_keys.include?(key) || key == :action || key == :controller non_path_parameters[key] = value else @@ -44,48 +62,80 @@ module ActionController end end - path_parameters[:controller] = controller_path - path_parameters[:action] = action + if get? + if self.query_string.blank? + self.query_string = non_path_parameters.to_query + end + else + if ENCODER.should_multipart?(non_path_parameters) + @env['CONTENT_TYPE'] = ENCODER.content_type + data = ENCODER.build_multipart non_path_parameters + else + @env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded' + + # FIXME: setting `request_parametes` is normally handled by the + # params parser middleware, and we should remove this roundtripping + # when we switch to caling `call` on the controller + + case content_mime_type.ref + when :json + data = ActiveSupport::JSON.encode(non_path_parameters) + params = ActiveSupport::JSON.decode(data).with_indifferent_access + self.request_parameters = params + when :xml + data = non_path_parameters.to_xml + params = Hash.from_xml(data)['hash'] + self.request_parameters = params + when :url_encoded_form + data = non_path_parameters.to_query + else + raise "Unknown Content-Type: #{content_type}" + end + end - # Clear the combined params hash in case it was already referenced. - @env.delete("action_dispatch.request.parameters") + @env['CONTENT_LENGTH'] = data.length.to_s + @env['rack.input'] = StringIO.new(data) + end - # Clear the filter cache variables so they're not stale - @filtered_parameters = @filtered_env = @filtered_path = nil + @env["PATH_INFO"] ||= generated_path + path_parameters[:controller] = controller_path + path_parameters[:action] = action - data = request_parameters.to_query - @env['CONTENT_LENGTH'] = data.length.to_s - @env['rack.input'] = StringIO.new(data) + self.path_parameters = path_parameters end - def recycle! - @formats = nil - @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } - @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @method = @request_method = nil - @fullpath = @ip = @remote_ip = @protocol = nil - @env['action_dispatch.request.query_parameters'] = {} - end + ENCODER = Class.new do + include Rack::Test::Utils + + def should_multipart?(params) + # FIXME: lifted from Rack-Test. We should push this separation upstream + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when Rack::Test::UploadedFile + multipart = true + end + } + params.values.each(&query) + multipart + end - private + public :build_multipart - def default_env - DEFAULT_ENV - end + def content_type + "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" + end + end.new end class TestResponse < ActionDispatch::TestResponse - def recycle! - initialize - end end class LiveTestResponse < Live::Response - def recycle! - @body = nil - initialize - end - def body @body ||= super end @@ -359,19 +409,6 @@ module ActionController end alias xhr :xml_http_request - def paramify_values(hash_or_array_or_value) - case hash_or_array_or_value - when Hash - Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] - when Array - hash_or_array_or_value.map {|i| paramify_values(i)} - when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile - hash_or_array_or_value - else - hash_or_array_or_value.to_param - end - end - # Simulate a HTTP request to +action+ by specifying request method, # parameters and set/volley the response. # @@ -431,10 +468,6 @@ module ActionController parameters ||= {} - # Ensure that numbers and symbols passed as params are converted to - # proper params, as is the case when engaging rack. - parameters = paramify_values(parameters) if html_format?(parameters) - if format.present? parameters[:format] = format end @@ -449,8 +482,9 @@ module ActionController @request.env['HTTP_COOKIE'] = cookies.to_header @request.env['action_dispatch.cookies'] = nil - @request.recycle! - @response.recycle! + @request = TestRequest.new scrub_env!(@request.env), @request.session + @response = build_response @response_klass + @response.request = @request @controller.recycle! @request.env['REQUEST_METHOD'] = http_method @@ -472,7 +506,7 @@ module ActionController @controller.request = @request @controller.response = @response - build_request_uri(controller_class_name, action, parameters) + @request.env["SCRIPT_NAME"] ||= @controller.config.relative_url_root @controller.recycle! @controller.process(action) @@ -497,6 +531,7 @@ module ActionController @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end + @request.query_string = '' @response end @@ -504,11 +539,11 @@ module ActionController def setup_controller_request_and_response @controller = nil unless defined? @controller - response_klass = TestResponse + @response_klass = TestResponse if klass = self.class.controller_class if klass < ActionController::Live - response_klass = LiveTestResponse + @response_klass = LiveTestResponse end unless @controller begin @@ -519,9 +554,8 @@ module ActionController end end - @request = build_request - @request.env["rack.request.cookie_hash"] = {}.with_indifferent_access - @response = build_response response_klass + @request = TestRequest.create + @response = build_response @response_klass @response.request = @request if @controller @@ -530,10 +564,6 @@ module ActionController end end - def build_request - TestRequest.new - end - def build_response(klass) klass.new end @@ -547,6 +577,14 @@ module ActionController private + def scrub_env!(env) + env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + env.delete 'action_dispatch.request.query_parameters' + env.delete 'action_dispatch.request.request_parameters' + env + end + def process_with_kwargs(http_method, action, *args) if kwarg_request?(args) args.first.merge!(method: http_method) @@ -593,23 +631,6 @@ module ActionController end end - def build_request_uri(controller_class_name, action, parameters) - unless @request.env["PATH_INFO"] - options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters - options.update( - :controller => controller_class_name, - :action => action, - :relative_url_root => nil, - :_recall => @request.path_parameters) - - url, query_string = @routes.path_for(options).split("?", 2) - - @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root - @request.env["PATH_INFO"] = url - @request.env["QUERY_STRING"] = query_string || "" - end - end - def html_format?(parameters) return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..e2b3b06fd8 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -13,40 +13,35 @@ module ActionDispatch end end - DEFAULT_PARSERS = { Mime::JSON => :json } + DEFAULT_PARSERS = { + Mime::JSON => lambda { |raw_post| + data = ActiveSupport::JSON.decode(raw_post) + data = {:_json => data} unless data.is_a?(Hash) + Request::Utils.deep_munge(data).with_indifferent_access + } + } def initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) end def call(env) - if params = parse_formatted_parameters(env) - env["action_dispatch.request.request_parameters"] = params - end + default = env["action_dispatch.request.request_parameters"] + env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default) @app.call(env) end private - def parse_formatted_parameters(env) + def parse_formatted_parameters(env, parsers, default) request = Request.new(env) - return false if request.content_length.zero? + return default if request.content_length.zero? - strategy = @parsers[request.content_mime_type] + strategy = parsers.fetch(request.content_mime_type) { return default } - return false unless strategy + strategy.call(request.raw_post) - case strategy - when Proc - strategy.call(request.raw_post) - when :json - data = ActiveSupport::JSON.decode(request.raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access - else - false - end rescue => e # JSON or Ruby code block errors logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index c94eea9134..543c7b78a1 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -183,7 +183,7 @@ module ActionDispatch end # Assume given controller - request = ActionController::TestRequest.new + request = ActionController::TestRequest.create if path =~ %r{://} fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 06e9abdd62..ad1a7f7109 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -7,17 +7,19 @@ module ActionDispatch 'HTTP_HOST' => 'test.host', 'REMOTE_ADDR' => '0.0.0.0', 'HTTP_USER_AGENT' => 'Rails Testing', - "rack.request.cookie_hash" => {}.with_indifferent_access ) - def self.new(env = {}) - super + # Create a new test request with default `env` values + def self.create(env = {}) + env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application + env["rack.request.cookie_hash"] ||= {}.with_indifferent_access + new(default_env.merge(env)) end - def initialize(env = {}) - env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application - super(default_env.merge(env)) + def self.default_env + DEFAULT_ENV end + private_class_method :default_env def request_method=(method) @env['REQUEST_METHOD'] = method.to_s.upcase @@ -63,11 +65,5 @@ module ActionDispatch @env.delete('action_dispatch.request.accepts') @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") end - - private - - def default_env - DEFAULT_ENV - end end end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 3240185414..b7fe253de7 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -127,8 +127,6 @@ class PerformActionTest < ActionController::TestCase # a more accurate simulation of what happens in "real life". @controller.logger = ActiveSupport::Logger.new(nil) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 4f5af85fea..5698159eba 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -22,8 +22,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = { controller: 'posts', action: 'index' } - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -52,8 +50,6 @@ class FragmentCachingTest < ActionController::TestCase @controller.perform_caching = true @controller.cache_store = @store @params = {:controller => 'posts', :action => 'index'} - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @controller.params = @params @controller.request = @request @controller.response = @response @@ -166,6 +162,8 @@ class FunctionalCachingController < CachingController end def formatted_fragment_cached_with_variant + request.variant = :phone if params[:v] == "phone" + respond_to do |format| format.html.phone format.html @@ -183,8 +181,6 @@ class FunctionalFragmentCachingTest < ActionController::TestCase @controller = FunctionalCachingController.new @controller.perform_caching = true @controller.cache_store = @store - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_fragment_caching @@ -268,9 +264,7 @@ CACHED def test_fragment_caching_with_variant - @request.variant = :phone - - get :formatted_fragment_cached_with_variant, format: "html" + get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone } assert_response :success expected_body = "<body>\n<p>PHONE</p>\n</body>\n" diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 9b0487841f..8a6a51c263 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -757,9 +757,8 @@ class FilterTest < ActionController::TestCase def test_dynamic_dispatch %w(foo bar baz).each do |action| - request = ActionController::TestRequest.new - request.query_parameters[:choose] = action - response = DynamicDispatchController.action(action).call(request.env).last + @request.query_parameters[:choose] = action + response = DynamicDispatchController.action(action).call(@request.env).last assert_equal action, response.body end end @@ -839,8 +838,6 @@ class FilterTest < ActionController::TestCase private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new process(action) end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index e263ed341f..a96cfa3bf4 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -73,14 +73,8 @@ module LocalAbcHelper end class HelperPathsTest < ActiveSupport::TestCase - def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - def test_helpers_paths_priority - request = ActionController::TestRequest.new - responses = HelpersPathsController.action(:index).call(request.env) + responses = HelpersPathsController.action(:index).call(ActionController::TestRequest::DEFAULT_ENV.dup) # helpers1_pack was given as a second path, so pack1_helper should be # included as the second one @@ -141,8 +135,7 @@ class HelperTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(ActionController::TestRequest::DEFAULT_ENV.dup) end def test_helper_for_nested_controller @@ -249,7 +242,7 @@ class HelperTest < ActiveSupport::TestCase end -class IsolatedHelpersTest < ActiveSupport::TestCase +class IsolatedHelpersTest < ActionController::TestCase class A < ActionController::Base def index render :inline => '<%= shout %>' @@ -273,13 +266,11 @@ class IsolatedHelpersTest < ActiveSupport::TestCase end def call_controller(klass, action) - request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(@request.env) end def setup - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + super @request.action = 'index' end diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index 77a2f68b1c..e5d698d5c2 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -1,11 +1,7 @@ require 'abstract_unit' require 'stringio' -class ActionController::TestRequestTest < ActiveSupport::TestCase - - def setup - @request = ActionController::TestRequest.new - end +class ActionController::TestRequestTest < ActionController::TestCase def test_test_request_has_session_options_initialized assert @request.session_options diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 82c808754c..7c17aed632 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -484,11 +484,10 @@ end class FreeCookieControllerTest < ActionController::TestCase def setup @controller = FreeCookieController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @token = "cf50faa3fe97702ca1ae" SecureRandom.stubs(:base64).returns(@token) + super end def test_should_not_render_form_with_token_tag diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index f3da2df3ef..5a279639cc 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1207,8 +1207,6 @@ class ResourcesTest < ActionController::TestCase @controller = "#{options[:options][:controller].camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new get :index, params: options[:options] options[:options].delete :action @@ -1277,8 +1275,6 @@ class ResourcesTest < ActionController::TestCase (options[:options] ||= {})[:controller] ||= singleton_name.to_s.pluralize @controller = "#{options[:options][:controller].camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new get :show, params: options[:options] options[:options].delete :action diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 36c57ec9b2..c0ddcf7f50 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -34,8 +34,6 @@ class SendFileTest < ActionController::TestCase def setup @controller = SendFileController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new end def test_file_nostream diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 86ffb898ad..b991232a14 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -45,11 +45,11 @@ class TestCaseTest < ActionController::TestCase end def test_params - render text: params.inspect + render text: ::JSON.dump(params) end def test_query_parameters - render text: request.query_parameters.inspect + render text: ::JSON.dump(request.query_parameters) end def test_request_parameters @@ -158,8 +158,6 @@ XML def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do @@ -229,7 +227,7 @@ XML def test_document_body_and_params_with_post post :test_params, params: { id: 1 } - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_document_body_with_post @@ -485,7 +483,7 @@ XML assert_deprecated { get :test_params, page: { name: "Page name", month: '4', year: '2004', day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -504,7 +502,7 @@ XML day: '6' } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( { 'controller' => 'test_case_test/test', 'action' => 'test_params', @@ -516,8 +514,8 @@ XML def test_query_param_named_action get :test_query_parameters, params: {action: 'foobar'} - parsed_params = eval(@response.body) - assert_equal({action: 'foobar'}, parsed_params) + parsed_params = JSON.parse(@response.body) + assert_equal({'action' => 'foobar'}, parsed_params) end def test_request_param_named_action @@ -536,7 +534,7 @@ XML } }, session: { 'foo' => 'bar' }, flash: { notice: 'created' } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, @@ -551,7 +549,7 @@ XML get :test_params, params: { page: { name: "Page name", month: 4, year: 2004, day: 6 } } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, @@ -561,17 +559,17 @@ XML def test_params_passing_with_fixnums_when_not_html_request get :test_params, params: { format: 'json', count: 999 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', - 'format' => 'json', 'count' => 999 }, + 'format' => 'json', 'count' => '999' }, parsed_params ) end def test_params_passing_path_parameter_is_string_when_not_html_request get :test_params, params: { format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -581,7 +579,7 @@ XML def test_deprecated_params_passing_path_parameter_is_string_when_not_html_request assert_deprecated { get :test_params, format: 'json', id: 1 } - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'format' => 'json', 'id' => '1' }, @@ -595,7 +593,7 @@ XML frozen: 'icy'.freeze, frozens: ['icy'.freeze].freeze, deepfreeze: { frozen: 'icy'.freeze }.freeze } end - parsed_params = eval(@response.body) + parsed_params = ::JSON.parse(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', 'frozen' => 'icy', 'frozens' => ['icy'], 'deepfreeze' => { 'frozen' => 'icy' }}, @@ -693,13 +691,13 @@ XML def test_deprecated_xhr_with_params assert_deprecated { xhr :get, :test_params, params: { id: 1 } } - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_xhr_with_params get :test_params, params: { id: 1 }, xhr: true - assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + assert_equal({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}, ::JSON.parse(@response.body)) end def test_xhr_with_session @@ -720,12 +718,6 @@ XML assert_equal 'it works', session[:symbol], "Test session hash should allow indifferent access" end - def test_header_properly_reset_after_get_request - get :test_params - @request.recycle! - assert_nil @request.instance_variable_get("@request_method") - end - def test_deprecated_params_reset_between_post_requests assert_deprecated { post :no_op, foo: "bar" } assert_equal "bar", @request.params[:foo] @@ -916,7 +908,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" assert_deprecated { - post :test_file_upload, file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + post :test_file_upload, file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -925,7 +917,7 @@ XML filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" post :test_file_upload, params: { - file: ActionDispatch::Http::UploadedFile.new(filename: path, type: "image/jpg", tempfile: File.open(path)) + file: Rack::Test::UploadedFile.new(path, "image/jpg", true) } assert_equal '159528', @response.body end @@ -957,10 +949,11 @@ class ResponseDefaultHeadersTest < ActionController::TestCase end end - setup do + def before_setup @original = ActionDispatch::Response.default_headers @defaults = { 'A' => '1', 'B' => '2' } ActionDispatch::Response.default_headers = @defaults + super end teardown do @@ -970,8 +963,6 @@ class ResponseDefaultHeadersTest < ActionController::TestCase def setup super @controller = TestController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index d9a1ae7d4f..5f2abc9606 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'controller/fake_controllers' -class UrlRewriterTests < ActiveSupport::TestCase +class UrlRewriterTests < ActionController::TestCase class Rewriter def initialize(request) @options = { @@ -16,7 +16,6 @@ class UrlRewriterTests < ActiveSupport::TestCase end def setup - @request = ActionController::TestRequest.new @params = {} @rewriter = Rewriter.new(@request) #.new(@request, @params) @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index a4c124070a..ede1cec4e6 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -2,7 +2,7 @@ require 'abstract_unit' class TestRequestTest < ActiveSupport::TestCase test "sane defaults" do - env = ActionDispatch::TestRequest.new.env + env = ActionDispatch::TestRequest.create.env assert_equal "GET", env.delete("REQUEST_METHOD") assert_equal "off", env.delete("HTTPS") @@ -27,7 +27,7 @@ class TestRequestTest < ActiveSupport::TestCase end test "cookie jar" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal({}, req.cookies) assert_equal nil, req.env["HTTP_COOKIE"] @@ -55,38 +55,38 @@ class TestRequestTest < ActiveSupport::TestCase test "does not complain when Rails.application is nil" do Rails.stubs(:application).returns(nil) - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal false, req.env.empty? end test "default remote address is 0.0.0.0" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal '0.0.0.0', req.remote_addr end test "allows remote address to be overridden" do - req = ActionDispatch::TestRequest.new('REMOTE_ADDR' => '127.0.0.1') + req = ActionDispatch::TestRequest.create('REMOTE_ADDR' => '127.0.0.1') assert_equal '127.0.0.1', req.remote_addr end test "default host is test.host" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'test.host', req.host end test "allows host to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_HOST' => 'www.example.com') + req = ActionDispatch::TestRequest.create('HTTP_HOST' => 'www.example.com') assert_equal 'www.example.com', req.host end test "default user agent is 'Rails Testing'" do - req = ActionDispatch::TestRequest.new + req = ActionDispatch::TestRequest.create({}) assert_equal 'Rails Testing', req.user_agent end test "allows user agent to be overridden" do - req = ActionDispatch::TestRequest.new('HTTP_USER_AGENT' => 'GoogleBot') + req = ActionDispatch::TestRequest.create('HTTP_USER_AGENT' => 'GoogleBot') assert_equal 'GoogleBot', req.user_agent end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 069d181674..1f6bb31cd4 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,8 @@ +* Allow defining explicit collection caching using a `# Template Collection: ...` + directive inside templates. + + *Dov Murik* + * Asset helpers raise `ArgumentError` when `nil` is passed as a source. *Anton Kolomiychuk* diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 8945575860..797d029317 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -137,6 +137,21 @@ module ActionView # The automatic cache multi read can be turned off like so: # # <%= render @notifications, cache: false %> + # + # === Explicit Collection Caching + # + # If the partial template doesn't start with a clean cache call as + # mentioned above, you can still benefit from collection caching by + # adding a special comment format anywhere in the template, like: + # + # <%# Template Collection: notification %> + # <% my_helper_that_calls_cache(some_arg, notification) do %> + # <%= notification.name %> + # <% end %> + # + # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, + # so it's important that you type it out just so. + # You can only declare one collection in a partial template file. def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index c216d4401f..6a3d01667d 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -206,6 +206,11 @@ module ActionView # +plural+ is supplied, it will use that when count is > 1, otherwise # it will use the Inflector to determine the plural form. # + # If passed an optional +locale:+ parameter, the word will be pluralized + # using rules defined for that language (you must define your own + # inflection rules for languages other than English). See + # ActiveSupport::Inflector.pluralize + # # pluralize(1, 'person') # # => 1 person # @@ -217,11 +222,14 @@ module ActionView # # pluralize(0, 'person') # # => 0 people - def pluralize(count, singular, plural = nil) + # + # pluralize(2, 'Person', locale: :de) + # # => 2 Personen + def pluralize(count, singular, plural = nil, locale: nil) word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else - plural || singular.pluralize + plural || singular.pluralize(locale) end "#{count || 0} #{word}" diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 377ceb534a..d8585514d5 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,7 @@ module ActionView @source = source @identifier = identifier @handler = handler - @cache_name = extract_resource_cache_call_name + @cache_name = extract_resource_cache_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -351,9 +351,18 @@ module ActionView ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block) end - def extract_resource_cache_call_name - $1 if @handler.respond_to?(:resource_cache_call_pattern) && - @source =~ @handler.resource_cache_call_pattern + EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ + + def extract_resource_cache_name + if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match + match[:resource_name] + end + end + + def resource_cache_call_match + if @handler.respond_to?(:resource_cache_call_pattern) + @source.match(@handler.resource_cache_call_pattern) + end end def inferred_cache_name diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index da96347e4d..1f8459c24b 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -125,7 +125,7 @@ module ActionView # Returns Regexp to extract a cached resource's name from a cache call at the # first line of a template. - # The extracted cache name is expected in $1. + # The extracted cache name is captured as :resource_name. # # <% cache notification do %> # => notification # @@ -138,7 +138,14 @@ module ActionView # # <% cache notification.event do %> # => nil def resource_cache_call_pattern - /\A(?:<%#.*%>)*\s*<%\s*cache\(?\s*(\w+)[\s\)]/m + /\A + (?:<%\#.*%>)* # optional initial comment + \s* # followed by optional spaces or newlines + <%\s*cache[\(\s] # followed by an ERB call to cache + \s* # followed by optional spaces or newlines + (?<resource_name>\w+) # capture the cache call argument as :resource_name + [\s\)] # followed by a space or close paren + /xm end private diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 06810ad14d..b4f36c1f78 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -24,7 +24,7 @@ module ActionView def initialize super self.class.controller_path = "" - @request = ActionController::TestRequest.new + @request = ActionController::TestRequest.create @response = ActionController::TestResponse.new @request.env.delete('PATH_INFO') diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index 7fba9ff8ff..2dd27358f7 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -23,7 +23,7 @@ class ViewLoadPathsTest < ActionController::TestCase end def setup - @request = ActionController::TestRequest.new + @request = ActionController::TestRequest.create @response = ActionController::TestResponse.new @controller = TestController.new @paths = TestController.view_paths diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index d17034e88f..d3b51cd629 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -194,14 +194,15 @@ class TestERBTemplate < ActiveSupport::TestCase [ "<%= 'Hello' %>", "<% cache_customer = 42 %>", - "<% cache customer.name do %><% end %>" + "<% cache customer.name do %><% end %>", + "<% my_cache customer do %><% end %>" ].each do |body| template = new_template(body, virtual_path: "test/foo/_customer") assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching" end end - def test_eligible_for_collection_caching_with_cache_call + def test_eligible_for_collection_caching_with_cache_call_or_explicit [ "<% cache customer do %><% end %>", "<% cache(customer) do %><% end %>", @@ -213,7 +214,8 @@ class TestERBTemplate < ActiveSupport::TestCase "<%# comment %><% cache customer do %><% end %>", "<%# comment %>\n<% cache customer do %><% end %>", "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>", - "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>" + "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>", + "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>" ].each do |body| template = new_template(body, virtual_path: "test/foo/_customer") assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching" diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index f1b84c4786..5791f33069 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -383,6 +383,18 @@ class TextHelperTest < ActionView::TestCase assert_equal("12 berries", pluralize(12, "berry")) end + def test_pluralization_with_locale + ActiveSupport::Inflector.inflections(:de) do |inflect| + inflect.plural(/(person)$/i, '\1en') + inflect.singular(/(person)en$/i, '\1') + end + + assert_equal("2 People", pluralize(2, "Person", locale: :en)) + assert_equal("2 Personen", pluralize(2, "Person", locale: :de)) + + ActiveSupport::Inflector.inflections(:de).clear + end + def test_cycle_class value = Cycle.new("one", 2, "3") assert_equal("one", value.to_s) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 3fa63917d0..f6d171bec6 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -27,14 +27,6 @@ class ErrorsTest < ActiveModel::TestCase end end - def setup - @mock_generator = MiniTest::Mock.new - end - - def teardown - @mock_generator.verify - end - def test_delete errors = ActiveModel::Errors.new(self) errors[:foo] << 'omg' @@ -307,8 +299,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do assert_deprecated do person.errors.add_on_empty :name end @@ -317,9 +308,8 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message for multiple attributes" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - @mock_generator.expect(:call, nil, [:age, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + expected_calls = [ [:name, :empty, {}], [:age, :empty, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do assert_deprecated do person.errors.add_on_empty [:name, :age] end @@ -328,8 +318,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message with custom default message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :empty, { message: 'custom' }]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, { message: 'custom' }]) do assert_deprecated do person.errors.add_on_empty :name, message: 'custom' end @@ -339,8 +328,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_empty generates message with empty string value" do person = Person.new person.name = '' - @mock_generator.expect(:call, nil, [:name, :empty, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :empty, {}]) do assert_deprecated do person.errors.add_on_empty :name end @@ -349,8 +337,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, {}]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :blank, {}]) do assert_deprecated do person.errors.add_on_blank :name end @@ -359,9 +346,8 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message for multiple attributes" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, {}]) - @mock_generator.expect(:call, nil, [:age, :blank, {}]) - person.errors.stub(:generate_message, @mock_generator) do + expected_calls = [ [:name, :blank, {}], [:age, :blank, {}] ] + assert_called_with(person.errors, :generate_message, expected_calls) do assert_deprecated do person.errors.add_on_blank [:name, :age] end @@ -370,8 +356,7 @@ class ErrorsTest < ActiveModel::TestCase test "add_on_blank generates message with custom default message" do person = Person.new - @mock_generator.expect(:call, nil, [:name, :blank, { message: 'custom' }]) - person.errors.stub(:generate_message, @mock_generator) do + assert_called_with(person.errors, :generate_message, [:name, :blank, { message: 'custom' }]) do assert_deprecated do person.errors.add_on_blank :name, message: 'custom' end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 0d179ea9ad..c100646837 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -11,6 +11,7 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'minitest/mock' @@ -22,3 +23,7 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveModel::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 70b93a202b..8d97c85fd9 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -11,7 +11,6 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new I18n.backend.store_translations('en', errors: { messages: { custom: nil } }) - @mock_generator = MiniTest::Mock.new end def teardown @@ -19,7 +18,6 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.replace @old_load_path I18n.backend = @old_backend I18n.backend.reload! - @mock_generator.verify end def test_full_message_encoding @@ -32,8 +30,7 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, 'not found') - @mock_generator.expect(:call, "Person's name", [:name, default: 'Name']) - Person.stub(:human_attribute_name, @mock_generator) do + assert_called_with(Person, :human_attribute_name, [:name, default: 'Name'], returns: "Person's name") do assert_equal ["Person's name not found"], @person.errors.full_messages end end @@ -64,8 +61,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @mock_generator.expect(:call, nil, [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -76,8 +73,8 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_acceptance_of on generated message #{name}" do Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false) - @mock_generator.expect(:call, nil, [:title, :accepted, generate_message_options]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :accepted, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -88,8 +85,8 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_presence_of on generated message #{name}" do Person.validates_presence_of :title, validation_options - @mock_generator.expect(:call, nil, [:title, :blank, generate_message_options]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :blank, generate_message_options] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -100,8 +97,8 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :within on generated message when too short #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) - @mock_generator.expect(:call, nil, [:title, :too_short, generate_message_options.merge(count: 3)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :too_short, generate_message_options.merge(count: 3)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -113,8 +110,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_length_of for :too_long generated message #{name}" do Person.validates_length_of :title, validation_options.merge(within: 3..5) @person.title = 'this title is too long' - @mock_generator.expect(:call, nil, [:title, :too_long, generate_message_options.merge(count: 5)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :too_long, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, ) do @person.valid? end end @@ -125,8 +122,8 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :is on generated message #{name}" do Person.validates_length_of :title, validation_options.merge(is: 5) - @mock_generator.expect(:call, nil, [:title, :wrong_length, generate_message_options.merge(count: 5)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :wrong_length, generate_message_options.merge(count: 5)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -138,8 +135,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_format_of on generated message #{name}" do Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/) @person.title = '72x' - @mock_generator.expect(:call, nil, [:title, :invalid, generate_message_options.merge(value: '72x')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :invalid, generate_message_options.merge(value: '72x')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -151,8 +148,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_inclusion_of on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'z' - @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -164,8 +161,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_inclusion_of using :within on generated message #{name}" do Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'z' - @mock_generator.expect(:call, nil, [:title, :inclusion, generate_message_options.merge(value: 'z')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :inclusion, generate_message_options.merge(value: 'z')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -177,8 +174,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_exclusion_of generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -190,8 +187,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_exclusion_of using :within generated message #{name}" do Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :exclusion, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :exclusion, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -203,8 +200,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_numericality_of generated message #{name}" do Person.validates_numericality_of :title, validation_options @person.title = 'a' - @mock_generator.expect(:call, nil, [:title, :not_a_number, generate_message_options.merge(value: 'a')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :not_a_number, generate_message_options.merge(value: 'a')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -216,8 +213,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_numericality_of for :only_integer on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true) @person.title = '0.0' - @mock_generator.expect(:call, nil, [:title, :not_an_integer, generate_message_options.merge(value: '0.0')]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :not_an_integer, generate_message_options.merge(value: '0.0')] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -229,8 +226,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_numericality_of for :odd on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true) @person.title = 0 - @mock_generator.expect(:call, nil, [:title, :odd, generate_message_options.merge(value: 0)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :odd, generate_message_options.merge(value: 0)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end @@ -242,8 +239,8 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_numericality_of for :less_than on generated message #{name}" do Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0) @person.title = 1 - @mock_generator.expect(:call, nil, [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]) - @person.errors.stub(:generate_message, @mock_generator) do + call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)] + assert_called_with(@person.errors, :generate_message, call) do @person.valid? end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 9ee8b79da9..03c7943308 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -97,7 +97,7 @@ class ValidatesWithTest < ActiveModel::TestCase test "passes all configuration options to the validator class" do topic = Topic.new - validator = MiniTest::Mock.new + validator = Minitest::Mock.new validator.expect(:new, validator, [{foo: :bar, if: "1 == 1", class: Topic}]) validator.expect(:validate, nil, [topic]) validator.expect(:is_a?, false, [Symbol]) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 6ebbdbc3db..4ffaa666b9 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,12 @@ +* Fix not calling `#default` on `HashWithIndifferentAcess#to_hash` when only + `default_proc` is set, which could raise. + + *Simon Eskildsen* + +* Fix setting `default_proc` on `HashWithIndifferentAccess#dup` + + *Simon Eskildsen* + * Fix a range of values for parameters of the Time#change *Nikolay Kondratyev* diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 4f71f13971..63690a1342 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -188,7 +188,7 @@ module ActiveSupport # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| - new_hash.default = default + set_defaults(new_hash) end end @@ -247,7 +247,9 @@ module ActiveSupport # Convert to a regular hash with string keys. def to_hash - _new_hash = Hash.new(default) + _new_hash = Hash.new + set_defaults(_new_hash) + each do |key, value| _new_hash[key] = convert_value(value, for: :to_hash) end @@ -275,6 +277,14 @@ module ActiveSupport value end end + + def set_defaults(target) + if default_proc + target.default_proc = default_proc.dup + else + target.default = default + end + end end end diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb new file mode 100644 index 0000000000..e3cbe40308 --- /dev/null +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -0,0 +1,35 @@ +module ActiveSupport + module Testing + module MethodCallAssertions # :nodoc: + private + def assert_called(object, method_name, message = nil, times: 1) + times_called = 0 + + object.stub(method_name, -> { times_called += 1 }) { yield } + + error = "Expected #{method_name} to be called #{times} times, " \ + "but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + assert_equal times, times_called, error + end + + def assert_called_with(object, method_name, args = [], returns: nil) + mock = Minitest::Mock.new + + if args.all? { |arg| arg.is_a?(Array) } + args.each { |arg| mock.expect(:call, returns, arg) } + else + mock.expect(:call, returns, args) + end + + object.stub(method_name, mock) { yield } + + mock.verify + end + + def assert_not_called(object, method_name, message = nil, &block) + assert_called(object, method_name, message, times: 0, &block) + end + end + end +end
\ No newline at end of file diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 9b88a2dcc1..65a8edbabb 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -15,6 +15,7 @@ silence_warnings do end require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' ENV['NO_RELOAD'] = '1' require 'active_support' @@ -38,4 +39,7 @@ def jruby_skip(message = '') end require 'minitest/mock' -require 'mocha/setup' # FIXME: stop using mocha + +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 69aea6213f..74ceff44f9 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -133,37 +133,42 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store - Dalli::Client.expects(:new).with(%w[localhost], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_given_mem_cache mem_cache = Dalli::Client.new - Dalli::Client.expects(:new).never - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_not_dalli_client - Dalli::Client.expects(:new).never - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end end end def test_mem_cache_fragment_cache_store_with_multiple_servers - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], {}) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1' + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end end def test_mem_cache_fragment_cache_store_with_options - Dalli::Client.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 }) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal 'foo', store.options[:namespace] + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { :timeout => 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal 'foo', store.options[:namespace] + end end def test_object_assigned_fragment_cache_store @@ -224,13 +229,15 @@ module CacheStoreBehavior def test_fetch_without_cache_miss @cache.write('foo', 'bar') - @cache.expects(:write).never - assert_equal 'bar', @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_equal 'bar', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss - @cache.expects(:write).with('foo', 'baz', @cache.options) - assert_equal 'baz', @cache.fetch('foo') { 'baz' } + assert_called_with(@cache, :write, ['foo', 'baz', @cache.options]) do + assert_equal 'baz', @cache.fetch('foo') { 'baz' } + end end def test_fetch_with_cache_miss_passes_key_to_block @@ -245,15 +252,18 @@ module CacheStoreBehavior def test_fetch_with_forced_cache_miss @cache.write('foo', 'bar') - @cache.expects(:read).never - @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true)) - @cache.fetch('foo', :force => true) { 'bar' } + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ['foo', 'bar', @cache.options.merge(:force => true)]) do + @cache.fetch('foo', :force => true) { 'bar' } + end + end end def test_fetch_with_cached_nil @cache.write('foo', nil) - @cache.expects(:write).never - assert_nil @cache.fetch('foo') { 'baz' } + assert_not_called(@cache, :write) do + assert_nil @cache.fetch('foo') { 'baz' } + end end def test_should_read_and_write_hash @@ -304,8 +314,9 @@ module CacheStoreBehavior end def test_multi_with_objects - foo = stub(:title => 'FOO!', :cache_key => 'foo') - bar = stub(:cache_key => 'bar') + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new('foo', 'FOO!') + bar = cache_struct.new('bar') @cache.write('bar', 'BAM!') @@ -774,9 +785,10 @@ class FileStoreTest < ActiveSupport::TestCase end def test_log_exception_when_cache_read_fails - File.expects(:exist?).raises(StandardError, "failed") - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end end def test_cleanup_removes_all_expired_entries diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index e10bee5e00..663f782611 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -524,6 +524,10 @@ class HashExtTest < ActiveSupport::TestCase end def test_indifferent_reverse_merging + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value') hash.reverse_merge!(:some => 'noclobber', :another => 'clobber') assert_equal 'value', hash[:some] @@ -961,10 +965,11 @@ class HashExtTest < ActiveSupport::TestCase assert_raise(RuntimeError) { original.except!(:a) } end - def test_except_with_mocha_expectation_on_original + def test_except_does_not_delete_values_in_original original = { :a => 'x', :b => 'y' } - original.expects(:delete).never - original.except(:a) + assert_not_called(original, :delete) do + original.except(:a) + end end def test_compact @@ -998,6 +1003,37 @@ class HashExtTest < ActiveSupport::TestCase assert hash.key?('a') assert_equal 1, hash[:a] end + + def test_dup_with_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, v| raise "walrus" } + assert_nothing_raised { hash.dup } + end + + def test_dup_with_default_proc_sets_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| k + 1 } + new_hash = hash.dup + + assert_equal 3, new_hash[2] + + new_hash.default = 2 + assert_equal 2, new_hash[:non_existant] + end + + def test_to_hash_with_raising_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_nothing_raised { hash.to_hash } + end + + def test_new_from_hash_copying_default_should_not_raise_when_default_proc_does + hash = Hash.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_nothing_raised { HashWithIndifferentAccess.new_from_hash_copying_default(hash) } + end end class IWriteMyOwnXML diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 5b48bf328c..477a42114b 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' require 'active_support/core_ext/string/strip' +require 'yaml' class TimeWithZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -482,22 +483,24 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_method_missing_with_non_time_return_value - @twz.time.expects(:foo).returns('bar') + time = @twz.time + def time.foo; 'bar'; end assert_equal 'bar', @twz.foo end def test_date_part_value_methods twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone) - twz.expects(:method_missing).never - assert_equal 1999, twz.year - assert_equal 12, twz.month - assert_equal 31, twz.day - assert_equal 14, twz.hour - assert_equal 18, twz.min - assert_equal 17, twz.sec - assert_equal 500, twz.usec - assert_equal 5, twz.wday - assert_equal 365, twz.yday + assert_not_called(twz, :method_missing) do + assert_equal 1999, twz.year + assert_equal 12, twz.month + assert_equal 31, twz.day + assert_equal 14, twz.hour + assert_equal 18, twz.min + assert_equal 17, twz.sec + assert_equal 500, twz.usec + assert_equal 5, twz.wday + assert_equal 365, twz.yday + end end def test_usec_returns_0_when_datetime_is_wrapped diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 6dce7560dd..757e600646 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -721,12 +721,13 @@ class DependenciesTest < ActiveSupport::TestCase end def test_unloadable_constants_should_receive_callback - Object.const_set :C, Class.new + Object.const_set :C, Class.new { def self.before_remove_const; end } C.unloadable - C.expects(:before_remove_const).once - assert C.respond_to?(:before_remove_const) - ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_called(C, :before_remove_const, times: 1) do + assert C.respond_to?(:before_remove_const) + ActiveSupport::Dependencies.clear + assert !defined?(C) + end ensure remove_constants(:C) end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 20bd8ee5dd..7e8844b301 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -256,17 +256,17 @@ class DeprecationTest < ActiveSupport::TestCase end def test_deprecate_with_custom_deprecator - custom_deprecator = mock('Deprecator') do - expects(:deprecation_warning) - end + custom_deprecator = Struct.new(:deprecation_warning).new - klass = Class.new do - def method + assert_called_with(custom_deprecator, :deprecation_warning, [:method, nil]) do + klass = Class.new do + def method + end + deprecate :method, deprecator: custom_deprecator end - deprecate :method, deprecator: custom_deprecator - end - klass.new.method + klass.new.method + end end def test_deprecated_constant_with_deprecator_given diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb deleted file mode 100644 index 1facd691fa..0000000000 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'abstract_unit' -require 'active_support/hash_with_indifferent_access' - -class HashWithIndifferentAccessTest < ActiveSupport::TestCase - def test_reverse_merge - hash = HashWithIndifferentAccess.new key: :old_value - hash.reverse_merge! key: :new_value - assert_equal :old_value, hash[:key] - end - -end diff --git a/activesupport/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb index 2a0e8d20ed..998a6887c5 100644 --- a/activesupport/test/log_subscriber_test.rb +++ b/activesupport/test/log_subscriber_test.rb @@ -82,10 +82,11 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase def test_does_not_send_the_event_if_logger_is_nil ActiveSupport::LogSubscriber.logger = nil - @log_subscriber.expects(:some_event).never - ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber - instrument "some_event.my_log_subscriber" - wait + assert_not_called(@log_subscriber, :some_event) do + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + instrument "some_event.my_log_subscriber" + wait + end end def test_does_not_fail_with_non_namespaced_events diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb index 52ae266f01..dd33641ec2 100644 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ b/activesupport/test/multibyte_unicode_database_test.rb @@ -11,8 +11,9 @@ class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase UnicodeDatabase::ATTRIBUTES.each do |attribute| define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do - @ucd.expects(:load) - @ucd.send(attribute) + assert_called(@ucd, :load) do + @ucd.send(attribute) + end end end diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb new file mode 100644 index 0000000000..a9908aea0d --- /dev/null +++ b/activesupport/test/testing/method_call_assertions_test.rb @@ -0,0 +1,98 @@ +require 'abstract_unit' +require 'active_support/testing/method_call_assertions' + +class MethodCallAssertionsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + class Level + def increment; 1; end + def decrement; end + def <<(arg); end + end + + setup do + @object = Level.new + end + + def test_assert_called_with_defaults_to_expect_once + assert_called @object, :increment do + @object.increment + end + end + + def test_assert_called_more_than_once + assert_called(@object, :increment, times: 2) do + @object.increment + @object.increment + end + end + + def test_assert_called_failure + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment) do + # Call nothing... + end + end + + assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message + end + + def test_assert_called_with_message + error = assert_raises(Minitest::Assertion) do + assert_called(@object, :increment, 'dang it') do + # Call nothing... + end + end + + assert_match(/dang it.\nExpected increment/, error.message) + end + + def test_assert_called_with + assert_called_with(@object, :increment) do + @object.increment + end + end + + def test_assert_called_with_arguments + assert_called_with(@object, :<<, [ 2 ]) do + @object << 2 + end + end + + def test_assert_called_with_failure + assert_raises(MockExpectationError) do + assert_called_with(@object, :<<, [ 4567 ]) do + @object << 2 + end + end + end + + def test_assert_called_with_returns + assert_called_with(@object, :increment, returns: 1) do + @object.increment + end + end + + def test_assert_called_with_multiple_expected_arguments + assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do + @object << 1 + @object << 2 + end + end + + def test_assert_not_called + assert_not_called(@object, :decrement) do + @object.increment + end + end + + def test_assert_not_called_failure + error = assert_raises(Minitest::Assertion) do + assert_not_called(@object, :increment) do + @object.increment + end + end + + assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message + end +end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 34bb0e2995..00d40c4497 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'yaml' class TimeZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 22f3c0146a..dd545b56f5 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -330,6 +330,13 @@ class GuestsCleanupJob < ActiveJob::Base end ``` +### Deserialization + +GlobalID allows serializing full Active Record objects passed to `#perform`. + +If a passed record is deleted after the job is enqueued but before the `#perform` +method is called Active Job will raise an `ActiveJob::DeserializationError` +exception. Job Testing -------------- |