diff options
96 files changed, 1279 insertions, 441 deletions
@@ -50,7 +50,7 @@ instance_eval File.read local_gemfile if File.exists? local_gemfile platforms :mri do group :test do - gem 'ruby-prof' + gem 'ruby-prof', '0.10.8' end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 96dee33f7b..b819f2e613 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* `respond_to` and `respond_with` now raise ActionController::UnknownFormat instead + of directly returning head 406. The exception is rescued and converted to 406 + in the exception handling middleware. *Steven Soroka* + * Allows `assert_redirected_to` to match against a regular expression. *Andy Lindeman* * Add backtrace to development routing error page. *Richard Schneeman* @@ -12,7 +16,7 @@ * Make current object and counter (when it applies) variables accessible when rendering templates with :object / :collection. *Carlos Antonio da Silva* -* JSONP now uses mimetype application/javascript instead of application/json *omjokine* +* JSONP now uses mimetype application/javascript instead of application/json. *omjokine* * Allow to lazy load `default_form_builder` by passing a `String` instead of a constant. *Piotr Sarnacki* diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 9a9db0fe5f..90648c37ad 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -38,4 +38,7 @@ module ActionController class UnknownHttpMethod < ActionControllerError #:nodoc: end + + class UnknownFormat < ActionControllerError #:nodoc: + end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 5bdbde9ebb..2fcd933d32 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -20,6 +20,7 @@ module ActionController options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok location = options.delete(:location) + content_type = options.delete(:content_type) options.each do |key, value| headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s @@ -29,7 +30,7 @@ module ActionController self.location = url_for(location) if location if include_content_headers?(self.status) - self.content_type = Mime[formats.first] if formats + self.content_type = content_type || (Mime[formats.first] if formats) else headers.delete('Content-Type') headers.delete('Content-Length') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f467b74256..7917926978 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -375,8 +375,7 @@ module ActionController #:nodoc: lookup_context.rendered_format = lookup_context.formats.first collector else - head :not_acceptable - nil + raise ActionController::UnknownFormat end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 7e2316d01c..1f52c164de 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -166,8 +166,9 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:accessible_attributes) && model.accessible_attributes.present? - options[:include] = model.accessible_attributes.to_a + role = options.fetch(:as, :default) + if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present? + options[:include] = model.accessible_attributes(role).to_a elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 21997c4d79..ad02375f12 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -20,7 +20,12 @@ module ActionController ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| path = payload[:layout] - @layouts[path] += 1 + if path + @layouts[path] += 1 + if path =~ /^layouts\/(.*)/ + @layouts[$1] += 1 + end + end end ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| @@ -59,6 +64,15 @@ module ActionController # # assert that the exact template "admin/posts/new" was rendered # assert_template %r{\Aadmin/posts/new\Z} # + # # assert that the layout 'admin' was rendered + # assert_template :layout => 'admin' + # assert_template :layout => 'layouts/admin' + # assert_template :layout => :admin + # + # # assert that no layout was rendered + # assert_template :layout => nil + # assert_template :layout => false + # # # assert that the "_customer" partial was rendered twice # assert_template :partial => '_customer', :count => 2 # @@ -90,16 +104,17 @@ module ActionController end end when Hash - if expected_layout = options[:layout] + if options.key?(:layout) + expected_layout = options[:layout] msg = message || sprintf("expecting layout <%s> but action rendered <%s>", expected_layout, @layouts.keys) case expected_layout - when String - assert_includes @layouts.keys, expected_layout, msg + when String, Symbol + assert_includes @layouts.keys, expected_layout.to_s, msg when Regexp assert(@layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil + when nil, false assert(@layouts.empty?, msg) end end @@ -120,7 +135,7 @@ module ActionController options[:partial], @partials.keys) assert_includes @partials, expected_partial, msg end - else + elsif options.key?(:partial) assert @partials.empty?, "Expected no partials to be rendered" end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 796e0dbc45..9748956052 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' require 'active_support/inflector' require 'action_dispatch/http/headers' +require 'action_dispatch/request/session' require 'action_controller/metal/exceptions' module ActionDispatch @@ -220,11 +221,11 @@ module ActionDispatch end def session=(session) #:nodoc: - @env['rack.session'] = session + Session.set @env, session end def session_options=(options) - @env['rack.session.options'] = options + Session::Options.set @env, options end # Override Rack's GET method to support indifferent access diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 2c0c31de9d..771f075275 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request + class Request < Rack::Request def cookie_jar env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) end @@ -26,9 +26,9 @@ module ActionDispatch # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # - # # Sets a signed cookie, which prevents a user from tampering with its value. + # # Sets a signed cookie, which prevents users from tampering with its value. # # The cookie is signed by your app's <tt>config.secret_token</tt> value. - # # Rails generates this value by default when you create a new Rails app. + # # It can be read using the signed method <tt>cookies.signed[:key]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -39,9 +39,10 @@ module ActionDispatch # # Examples for reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index c0532c80c4..982f6641bf 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -10,6 +10,7 @@ module ActionDispatch 'AbstractController::ActionNotFound' => :not_found, 'ActionController::MethodNotAllowed' => :method_not_allowed, 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity ) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index fd0ce62a3b..17776c2356 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,5 +1,5 @@ module ActionDispatch - class Request + class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. @@ -218,7 +218,7 @@ module ActionDispatch def call(env) @app.call(env) ensure - session = env['rack.session'] || {} + session = Request::Session.find(env) || {} flash_hash = env[KEY] if flash_hash diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index e82132b445..64159fa8e7 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -2,6 +2,7 @@ require 'rack/utils' require 'rack/request' require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' +require 'action_dispatch/request/session' require 'active_support/core_ext/object/blank' module ActionDispatch @@ -18,19 +19,6 @@ module ActionDispatch end end - module DestroyableSession - def destroy - clear - options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env - options ||= {} - @by.send(:destroy_session, @env, options[:id], options) if @by - options[:id] = nil - @loaded = false - end - end - - ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession - module Compatibility def initialize(app, options = {}) options[:key] ||= '_session_id' @@ -77,9 +65,20 @@ module ActionDispatch end end + module SessionObject # :nodoc: + def prepare_session(env) + Request::Session.create(self, env, @default_options) + end + + def loaded_session?(session) + !session.is_a?(Request::Session) || session.loaded? + end + end + class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck + include SessionObject private diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index a4866f5a8f..dbcf703ec3 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -43,6 +43,7 @@ module ActionDispatch class CookieStore < Rack::Session::Cookie include Compatibility include StaleSessionCheck + include SessionObject private diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 4dd9a946c2..38a737cd2b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -6,6 +6,7 @@ module ActionDispatch class MemCacheStore < Rack::Session::Memcache include Compatibility include StaleSessionCheck + include SessionObject def initialize(app, options = {}) require 'memcache' 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 0c5bafa666..823f5d25b6 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 @@ -12,8 +12,8 @@ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") - def debug_hash(hash) - hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\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") end unless self.class.method_defined?(:debug_hash) %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb new file mode 100644 index 0000000000..4ad7071820 --- /dev/null +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -0,0 +1,166 @@ +require 'rack/session/abstract/id' + +module ActionDispatch + class Request < Rack::Request + # SessionHash is responsible to lazily load the session from store. + class Session # :nodoc: + ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: + ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + + def self.create(store, env, default_options) + session_was = find env + session = Request::Session.new(store, env) + session.merge! session_was if session_was + + set(env, session) + Options.set(env, Request::Session::Options.new(store, env, default_options)) + session + end + + def self.find(env) + env[ENV_SESSION_KEY] + end + + def self.set(env, session) + env[ENV_SESSION_KEY] = session + end + + class Options #:nodoc: + def self.set(env, options) + env[ENV_SESSION_OPTIONS_KEY] = options + end + + def self.find(env) + env[ENV_SESSION_OPTIONS_KEY] + end + + def initialize(by, env, default_options) + @by = by + @env = env + @delegate = default_options.dup + end + + def [](key) + if key == :id + @delegate.fetch(key) { + @delegate[:id] = @by.send(:extract_session_id, @env) + } + else + @delegate[key] + end + end + + def []=(k,v); @delegate[k] = v; end + def to_hash; @delegate.dup; end + def values_at(*args); @delegate.values_at(*args); end + end + + def initialize(by, env) + @by = by + @env = env + @delegate = {} + @loaded = false + @exists = nil # we haven't checked yet + end + + def options + Options.find @env + end + + def destroy + clear + options = self.options || {} + @by.send(:destroy_session, @env, options[:id], options) + options[:id] = nil + @loaded = false + end + + def [](key) + load_for_read! + @delegate[key.to_s] + end + + def has_key?(key) + load_for_read! + @delegate.key?(key.to_s) + end + alias :key? :has_key? + alias :include? :has_key? + + def []=(key, value) + load_for_write! + @delegate[key.to_s] = value + end + + def clear + load_for_write! + @delegate.clear + end + + def to_hash + load_for_read! + @delegate.dup.delete_if { |_,v| v.nil? } + end + + def update(hash) + load_for_write! + @delegate.update stringify_keys(hash) + end + + def delete(key) + load_for_write! + @delegate.delete key.to_s + end + + def inspect + if loaded? + super + else + "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>" + end + end + + def exists? + return @exists unless @exists.nil? + @exists = @by.send(:session_exists?, @env) + end + + def loaded? + @loaded + end + + def empty? + load_for_read! + @delegate.empty? + end + + def merge!(other) + load_for_write! + @delegate.merge!(other) + end + + private + + def load_for_read! + load! if !loaded? && exists? + end + + def load_for_write! + load! unless loaded? + end + + def load! + id, session = @by.load_session @env + options[:id] = id + @delegate.replace(stringify_keys(session)) + @loaded = true + end + + def stringify_keys(other) + other.each_with_object({}) { |(key, value), hash| + hash[key.to_s] = value + } + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 4ea3937057..2a7d540517 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -535,7 +535,8 @@ module ActionDispatch private def map_method(method, args, &block) options = args.extract_options! - options[:via] = method + options[:via] = method + options[:path] ||= args.first if args.first.is_a?(String) match(*args, options, &block) self end @@ -1509,7 +1510,7 @@ module ActionDispatch prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] - path = if canonical_action?(action, path.blank?) + if canonical_action?(action, path.blank?) prefix.to_s else "#{prefix}/#{action_path(action, path)}" diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 7abd7bd008..8fc0f283fc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -188,7 +188,8 @@ module ActionDispatch remove_possible_method :#{selector} def #{selector}(*args) if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? - options = #{options.inspect}.merge!(url_options) + options = #{options.inspect} + options.merge!(url_options) if respond_to?(:url_options) options[:path] = "#{optimized_helper(route)}" ActionDispatch::Http::URL.url_for(options) else diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 6bd8e62e0d..f0a593d2c1 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -66,12 +66,12 @@ module ActionView # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {}) - unless include_seconds_or_options.is_a?(Hash) + if include_seconds_or_options.is_a?(Hash) + options = include_seconds_or_options + else ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " + "as a part of options hash, not a boolean argument", caller options[:include_seconds] ||= !!include_seconds_or_options - else - options = include_seconds_or_options end from_time = from_time.to_time if from_time.respond_to?(:to_time) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 8e8a56c65c..248cc2f6a3 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -408,7 +408,7 @@ module ActionView # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", :disable_with => "Editing...", :class => "edit_button" - # # => <input class="edit_button" data-disable_with="Editing..." name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable-with="Editing..." name="commit" type="submit" value="Edit" /> # # submit_tag "Save", :confirm => "Are you sure?" # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb index 6a1479069f..4e33e79a36 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -49,7 +49,7 @@ module ActionView accept = if current_value.respond_to?(:call) current_value.call(item) else - Array(current_value).include?(value) + Array(current_value).map(&:to_s).include?(value.to_s) end if accept diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index d0f716cc80..1145f348c2 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -303,7 +303,10 @@ module ActionView # # <%= button_to "Create", :action => "create", :remote => true, :form => { "data-type" => "json" } %> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> - # # <div><input value="Create" type="submit" /></div> + # # <div> + # # <input value="Create" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> + # # </div> # # </form>" # # @@ -312,7 +315,8 @@ module ActionView # # => "<form method="post" action="/images/delete/1" class="button_to"> # # <div> # # <input type="hidden" name="_method" value="delete" /> - # # <input data-confirm='Are you sure?' value="Delete" type="submit" /> + # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # @@ -323,6 +327,7 @@ module ActionView # # <div> # # <input name='_method' value='delete' type='hidden' /> # # <input value='Destroy' type='submit' disable_with='loading...' data-confirm='Are you sure?' /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # # diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index c5d5540510..87609fd5ff 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -343,8 +343,8 @@ module ActionView if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.") + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.") end self diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index f5f397c9c0..9b0d4d0f4c 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -76,6 +76,11 @@ class ActionPackAssertionsController < ActionController::Base render "test/hello_world", :layout => "layouts/standard" end + def render_with_layout_and_partial + @variable_for_layout = nil + render "test/hello_world_with_partial", :layout => "layouts/standard" + end + def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" @@ -473,11 +478,43 @@ class AssertTemplateTest < ActionController::TestCase end end + def test_fails_expecting_no_layout + get :render_with_layout + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :layout => nil + end + end + def test_passes_with_correct_layout get :render_with_layout assert_template :layout => "layouts/standard" end + def test_passes_with_layout_and_partial + get :render_with_layout_and_partial + assert_template :layout => "layouts/standard" + end + + def test_passed_with_no_layout + get :hello_world + assert_template :layout => nil + end + + def test_passed_with_no_layout_false + get :hello_world + assert_template :layout => false + end + + def test_passes_with_correct_layout_without_layouts_prefix + get :render_with_layout + assert_template :layout => "standard" + end + + def test_passes_with_correct_layout_symbol + get :render_with_layout + assert_template :layout => :standard + end + def test_assert_template_reset_between_requests get :hello_world assert_template 'test/hello_world' diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 877b91b563..9f2dbda25f 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -405,6 +405,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_request_with_bad_format + with_test_route_set do + xhr :get, '/get.php' + assert_equal 406, status + assert_response 406 + assert_response :not_acceptable + end + end + def test_get_with_query_string with_test_route_set do get '/get_with_params?foo=bar' diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index ac056319fc..bdcd5561a8 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -207,8 +207,9 @@ class RespondToControllerTest < ActionController::TestCase get :html_or_xml assert_equal 'HTML', @response.body - get :just_xml - assert_response 406 + assert_raises(ActionController::UnknownFormat) do + get :just_xml + end end def test_all @@ -239,8 +240,10 @@ class RespondToControllerTest < ActionController::TestCase assert_equal 'HTML', @response.body @request.accept = "text/javascript, text/html" - xhr :get, :just_xml - assert_response 406 + + assert_raises(ActionController::UnknownFormat) do + xhr :get, :just_xml + end end def test_json_or_yaml_with_leading_star_star @@ -495,9 +498,9 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_format - get :using_defaults, :format => "invalidformat" - assert_equal " ", @response.body - assert_equal "text/html", @response.content_type + assert_raises(ActionController::UnknownFormat) do + get :using_defaults, :format => "invalidformat" + end end end @@ -701,12 +704,14 @@ class RespondWithControllerTest < ActionController::TestCase def test_not_acceptable @request.accept = "application/xml" - get :using_resource_with_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_block + end @request.accept = "text/javascript" - get :using_resource_with_overwrite_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_overwrite_block + end end def test_using_resource_for_post_with_html_redirects_on_success @@ -984,8 +989,9 @@ class RespondWithControllerTest < ActionController::TestCase def test_clear_respond_to @controller = InheritedRespondWithController.new @request.accept = "text/html" - get :index - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :index + end end def test_first_in_respond_to_has_higher_priority diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index c4d2614200..fa1608b9df 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -174,7 +174,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_matching_model User.expects(:respond_to?).with(:accessible_attributes).returns(true) - User.expects(:accessible_attributes).twice.returns(["username"]) + User.expects(:accessible_attributes).with(:default).twice.returns(["username"]) with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' @@ -186,7 +186,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_specified_model with_default_wrapper_options do Person.expects(:respond_to?).with(:accessible_attributes).returns(true) - Person.expects(:accessible_attributes).twice.returns(["username"]) + Person.expects(:accessible_attributes).with(:default).twice.returns(["username"]) UsersController.wrap_parameters Person @@ -195,6 +195,19 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end + + def test_accessible_wrapped_keys_with_role_from_specified_model + with_default_wrapper_options do + Person.expects(:respond_to?).with(:accessible_attributes).returns(true) + Person.expects(:accessible_attributes).with(:admin).twice.returns(["username"]) + + UsersController.wrap_parameters Person, :as => :admin + + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + end + end def test_not_wrapping_abstract_model User.expects(:respond_to?).with(:accessible_attributes).returns(false) diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 10f62dad65..3d58c02338 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -102,12 +102,12 @@ class TestController < ActionController::Base end def conditional_hello_with_expires_in_with_public_with_more_keys - expires_in 1.minute, :public => true, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, 's-maxage' => 5.hours render :action => 'hello_world' end def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, :private => nil, 's-maxage' => 5.hours render :action => 'hello_world' end @@ -505,6 +505,14 @@ class TestController < ActionController::Base render :text => "hello world!" end + def head_created + head :created + end + + def head_created_with_application_json_content_type + head :created, :content_type => "application/json" + end + def head_with_location_header head :location => "/foo" end @@ -1177,6 +1185,19 @@ class RenderTest < ActionController::TestCase assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body end + def test_head_created + post :head_created + assert_blank @response.body + assert_response :created + end + + def test_head_created_with_application_json_content_type + post :head_created_with_application_json_content_type + assert_blank @response.body + assert_equal "application/json", @response.content_type + assert_response :created + end + def test_head_with_location_header get :head_with_location_header assert_blank @response.body @@ -1478,12 +1499,12 @@ class ExpiresInRenderTest < ActionController::TestCase def test_expires_in_header_with_additional_headers get :conditional_hello_with_expires_in_with_public_with_more_keys - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_in_old_syntax get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_now diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index bcb4e6a766..cd91064ab8 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1405,7 +1405,7 @@ class RouteSetTest < ActiveSupport::TestCase end end end - + def test_route_with_subdomain_and_constraints_must_receive_params name_param = nil set.draw do @@ -1418,7 +1418,7 @@ class RouteSetTest < ActiveSupport::TestCase set.recognize_path('http://subdomain.example.org/page/mypage')) assert_equal(name_param, 'mypage') end - + def test_route_requirement_recognize_with_ignore_case set.draw do get 'page/:name' => 'pages#show', diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index f7a746120e..536e35ab2e 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -37,6 +37,11 @@ class TestRoutingMount < ActionDispatch::IntegrationTest assert_equal "/sprockets -- /omg", response.body end + def test_mounting_works_with_nested_script_name + get "/foo/sprockets/omg", {}, 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/sprockets/omg' + assert_equal "/foo/sprockets -- /omg", response.body + end + def test_mounting_works_with_scope get "/its_a/sprocket/omg" assert_equal "/its_a/sprocket -- /omg", response.body diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb new file mode 100644 index 0000000000..4d24456ba6 --- /dev/null +++ b/actionpack/test/dispatch/request/session_test.rb @@ -0,0 +1,48 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + class Request + class SessionTest < ActiveSupport::TestCase + def test_create_adds_itself_to_env + env = {} + s = Session.create(store, env, {}) + assert_equal s, env[Rack::Session::Abstract::ENV_SESSION_KEY] + end + + def test_to_hash + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + assert_equal 'bar', s['foo'] + assert_equal({'foo' => 'bar'}, s.to_hash) + end + + def test_create_merges_old + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + + s1 = Session.create(store, env, {}) + refute_equal s, s1 + assert_equal 'bar', s1['foo'] + end + + def test_find + env = {} + assert_nil Session.find(env) + + s = Session.create(store, env, {}) + assert_equal s, Session.find(env) + end + + private + def store + Class.new { + def load_session(env); [1, {}]; end + def session_exists?(env); true; end + }.new + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index a96d2edcf9..e5345754cd 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -171,6 +171,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest post :preview, :on => :collection end end + + post 'new', :action => 'new', :on => :collection, :as => :new end resources :replies do @@ -876,6 +878,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_with_post_action_and_new_path_on_collection + post '/projects/new' + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_projects_path + end + def test_projects_involvements get '/projects/1/involvements' assert_equal 'involvements#index', @response.body @@ -2450,7 +2458,6 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest get "/foo/bar/baz" assert_equal "/pooh", @response.body end - end class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest @@ -2531,3 +2538,27 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest assert_equal "bar", @request.params[:bar] end end + +class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/foo' => ok, as: :foo + end + end + + include Routes.url_helpers + def app; Routes end + + test 'enabled when not mounted and default_url_options is empty' do + assert Routes.url_helpers.optimize_routes_generation? + end + + test 'named route called as singleton method' do + assert_equal '/foo', Routes.url_helpers.foo_path + end + + test 'named route called on included module' do + assert_equal '/foo', foo_path + end +end diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb new file mode 100644 index 0000000000..8daf3d3f5e --- /dev/null +++ b/actionpack/test/dispatch/session/abstract_store_test.rb @@ -0,0 +1,56 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + module Session + class AbstractStoreTest < ActiveSupport::TestCase + class MemoryStore < AbstractStore + def initialize(app) + @sessions = {} + super + end + + def get_session(env, sid) + sid ||= 1 + session = @sessions[sid] ||= {} + [sid, session] + end + + def set_session(env, sid, session, options) + @sessions[sid] = session + end + end + + def test_session_is_set + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + assert Request::Session.find @env + end + + def test_new_session_object_is_merged_with_old + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + session = Request::Session.find @env + session['foo'] = 'bar' + + as.call(@env) + session1 = Request::Session.find @env + + refute_equal session, session1 + assert_equal session.to_hash, session1.to_hash + end + + private + def app(&block) + @env = nil + lambda { |env| @env = env } + end + end + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index 985ff2e81a..e56e8ddc57 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module TestUrlGeneration class WithMountPoint < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new - Routes.draw { get "/foo", :to => "my_route_generating#index", :as => :foo } + include Routes.url_helpers class ::MyRouteGeneratingController < ActionController::Base include Routes.url_helpers @@ -12,7 +12,11 @@ module TestUrlGeneration end end - include Routes.url_helpers + Routes.draw do + get "/foo", :to => "my_route_generating#index", :as => :foo + + mount MyRouteGeneratingController.action(:index), at: '/bar' + end def _routes Routes @@ -30,11 +34,16 @@ module TestUrlGeneration assert_equal "/bar/foo", foo_path(:script_name => "/bar") end - test "the request's SCRIPT_NAME takes precedence over the routes'" do + test "the request's SCRIPT_NAME takes precedence over the route" do get "/foo", {}, 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes assert_equal "/new/foo", response.body end + test "the request's SCRIPT_NAME wraps the mounted app's" do + get '/new/bar/foo', {}, 'SCRIPT_NAME' => '/new', 'PATH_INFO' => '/bar/foo', 'action_dispatch.routes' => Routes + assert_equal "/new/bar/foo", response.body + end + test "handling http protocol with https set" do https! assert_equal "http://www.example.com/foo", foo_url(:protocol => "http") diff --git a/actionpack/test/fixtures/test/hello_world_with_partial.html.erb b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb new file mode 100644 index 0000000000..ec31545356 --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb @@ -0,0 +1,2 @@ +Hello world! +<%= render '/test/partial' %> diff --git a/actionpack/test/template/form_collections_helper_test.rb b/actionpack/test/template/form_collections_helper_test.rb index 4d878635ef..c73e80ed88 100644 --- a/actionpack/test/template/form_collections_helper_test.rb +++ b/actionpack/test/template/form_collections_helper_test.rb @@ -195,6 +195,15 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=checkbox][value=2][checked=checked]' end + test 'collection check boxes accepts selected string values as :checked option' do + collection = (1..3).map{|i| [i, "Category #{i}"] } + with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3'] + + assert_select 'input[type=checkbox][value=1][checked=checked]' + assert_select 'input[type=checkbox][value=3][checked=checked]' + assert_no_select 'input[type=checkbox][value=2][checked=checked]' + end + test 'collection check boxes accepts a single checked value' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3 diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index cdaca56ef2..e7f5f100bf 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -153,15 +153,15 @@ module RenderTestCases def test_render_partial_with_invalid_name e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") } assert_equal "The partial name (test/200) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_missing_filename e = assert_raises(ArgumentError) { @view.render(:partial => "test/") } assert_equal "The partial name (test/) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_incompatible_object diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a661a44f1f..31886c8212 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,11 @@ ## Rails 4.0.0 (unreleased) ## +* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by + default to avoid silent data loss. This can be disabled by specifying + `strict: false` in your `database.yml`. + + *Michael Pearson* + * Added default order to `first` to assure consistent results among diferent database engines. Introduced `take` as a replacement to the old behavior of `first`. diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 9af8e46120..1933ce2b46 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -158,7 +158,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.empty? + return nil if value.blank? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -169,14 +169,14 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? fast_string_to_time(string) || fallback_string_to_time(string) end def string_to_dummy_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? string_to_time "2000-01-01 #{string}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 92908d9599..350ccce03d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -253,6 +253,14 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 variable_assignments = ['SQL_AUTO_IS_NULL=0'] + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" + end + encoding = @config[:encoding] # make sure we set the encoding diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 724dbff1f0..0b6734b010 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -53,6 +53,7 @@ module ActiveRecord # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -404,6 +405,13 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) + end end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 68cf495025..4d5459939b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -511,8 +511,13 @@ module ActiveRecord else super end when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end when Numeric return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index eb8f4ad669..a869ed8c04 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/deep_dup' require 'thread' module ActiveRecord @@ -165,7 +166,7 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) @columns_hash = self.class.column_types.dup init_internals diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 8726442d0b..e278e62ce7 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,5 +1,10 @@ module ActiveRecord - module DynamicMatchers + module DynamicMatchers #:nodoc: + # This code in this file seems to have a lot of indirection, but the indirection + # is there to provide extension points for the active_record_deprecated_finders + # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # then we can remove the indirection. + def respond_to?(name, include_private = false) match = Method.match(self, name) match && match.valid? || super @@ -7,15 +12,6 @@ module ActiveRecord private - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. def method_missing(name, *arguments, &block) match = Method.match(self, name) @@ -28,28 +24,27 @@ module ActiveRecord end class Method - def self.match(model, name) - klass = klasses.find { |k| name =~ k.pattern } - klass.new(model, name) if klass - end + @matchers = [] - def self.klasses - [ - FindBy, FindAllBy, FindLastBy, FindByBang, ScopedBy, - FindOrInitializeBy, FindOrCreateBy, FindOrCreateByBang - ] - end + class << self + attr_reader :matchers - def self.pattern - /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ - end + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end - def self.prefix - raise NotImplementedError - end + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end - def self.suffix - '' + def prefix + raise NotImplementedError + end + + def suffix + '' + end end attr_reader :model, :name, :attribute_names @@ -77,20 +72,20 @@ module ActiveRecord end end - class Finder < Method + module Finder + # Extended in active_record_deprecated_finders def body - <<-CODE - result = #{result} - result && block_given? ? yield(result) : result - CODE + result end + # Extended in active_record_deprecated_finders def result - "scoped.apply_finder_options(options).#{finder}(#{attributes_hash})" + "#{finder}(#{attributes_hash})" end + # Extended in active_record_deprecated_finders def signature - attribute_names.join(', ') + ", options = {}" + attribute_names.join(', ') end def attributes_hash @@ -102,7 +97,10 @@ module ActiveRecord end end - class FindBy < Finder + class FindBy < Method + Method.matchers << self + include Finder + def self.prefix "find_by" end @@ -112,7 +110,10 @@ module ActiveRecord end end - class FindByBang < Finder + class FindByBang < Method + Method.matchers << self + include Finder + def self.prefix "find_by" end @@ -125,108 +126,5 @@ module ActiveRecord "find_by!" end end - - class FindAllBy < Finder - def self.prefix - "find_all_by" - end - - def finder - "where" - end - - def result - "#{super}.to_a" - end - end - - class FindLastBy < Finder - def self.prefix - "find_last_by" - end - - def finder - "where" - end - - def result - "#{super}.last" - end - end - - class ScopedBy < Finder - def self.prefix - "scoped_by" - end - - def body - "where(#{attributes_hash})" - end - end - - class Instantiator < Method - # This is nasty, but it doesn't matter because it will be deprecated. - def self.dispatch(klass, attribute_names, instantiator, args, block) - if args.length == 1 && args.first.is_a?(Hash) - attributes = args.first.stringify_keys - conditions = attributes.slice(*attribute_names) - rest = [attributes.except(*attribute_names)] - else - raise ArgumentError, "too few arguments" unless args.length >= attribute_names.length - - conditions = Hash[attribute_names.map.with_index { |n, i| [n, args[i]] }] - rest = args.drop(attribute_names.length) - end - - klass.where(conditions).first || - klass.create_with(conditions).send(instantiator, *rest, &block) - end - - def signature - "*args, &block" - end - - def body - "#{self.class}.dispatch(self, #{attribute_names.inspect}, #{instantiator.inspect}, args, block)" - end - - def instantiator - raise NotImplementedError - end - end - - class FindOrInitializeBy < Instantiator - def self.prefix - "find_or_initialize_by" - end - - def instantiator - "new" - end - end - - class FindOrCreateBy < Instantiator - def self.prefix - "find_or_create_by" - end - - def instantiator - "create" - end - end - - class FindOrCreateByBang < Instantiator - def self.prefix - "find_or_create_by" - end - - def self.suffix - "!" - end - - def instantiator - "create!" - end - end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 9796b0a321..a01e2f74ff 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -823,7 +823,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index a78e3d08e4..cc716bbfd1 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -51,13 +51,13 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago # def find_by(*args) - where(*args).first + where(*args).take end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. def find_by!(*args) - where(*args).first! + where(*args).take! end # Gives a record (or N records if a parameter is supplied) without any implied @@ -269,7 +269,7 @@ module ActiveRecord substitute = connection.substitute_at(column, bind_values.length) relation = where(table[primary_key].eq(substitute)) relation.bind_values += [[column, id]] - record = relation.first + record = relation.take unless record conditions = arel.where_sql @@ -309,7 +309,7 @@ module ActiveRecord def find_take if loaded? - @records.take(1).first + @records.first else @take ||= limit(1).to_a.first end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index fa2ba8d592..5e1c52c9ba 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -120,6 +120,19 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8e2b9ca9a5..684c7f5929 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -29,6 +29,22 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + # TODO: Below is a straight up copy/paste from mysql/connection_test.rb + # I'm not sure what the correct way is to share these tests between + # adapters in minitest. + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 172055f15c..f8a605b67c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -19,6 +19,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end + + def test_quote_float_nan + nan = 0.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'NaN'", @conn.quote(nan, c) + end + + def test_quote_float_infinity + infinity = 1.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'Infinity'", @conn.quote(infinity, c) + end end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 35e0045e4c..c1b0cb8886 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1909,7 +1909,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], + assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -2001,6 +2001,12 @@ class BasicsTest < ActiveRecord::TestCase assert_nil hash['firm_name'] end + def test_default_values_are_deeply_dupped + company = Company.new + company.description << "foo" + assert_equal "", Company.new.description + end + ["find_by", "find_by!"].each do |meth| test "#{meth} delegates to scoped" do record = stub diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index 4fcf8a33a4..4111a5f808 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -48,6 +48,34 @@ module ActiveRecord column.type_cast(false) end end + + def test_type_cast_time + column = Column.new("field", nil, "time") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, column.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") + end + end + + def test_type_cast_date + column = Column.new("field", nil, "date") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, column.type_cast(date_string).strftime("%F") + end end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 54801bd101..630acdbc46 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -113,6 +113,10 @@ class FinderTest < ActiveRecord::TestCase assert_equal [], Topic.find([]) end + def test_find_doesnt_have_implicit_ordering + assert_sql(/^((?!ORDER).)*$/) { Topic.find(1) } + end + def test_find_by_ids_missing_one assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 393e578219..2e87c5be2f 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -442,11 +442,11 @@ end class DynamicScopeMatchTest < ActiveRecord::TestCase def test_scoped_by_no_match - assert_nil ActiveRecord::DynamicMatchers::ScopedBy.match(nil, "not_scoped_at_all") + assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all") end def test_scoped_by - match = ActiveRecord::DynamicMatchers::ScopedBy.match(nil, "scoped_by_age_and_sex_and_location") + match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location") assert_not_nil match assert_equal %w(age sex location), match.attribute_names end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 3ef357e297..8cef4423c5 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -255,7 +255,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal nil, Developer.none.calculate(:average, 'salary') end end - + def test_null_relation_metadata_methods assert_equal "", Developer.none.to_sql assert_equal({}, Developer.none.where_values_hash) @@ -1287,6 +1287,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal nil, Post.scoped.find_by("1 = 0") end + test "find_by doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1299,6 +1303,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:eager_other), Post.order(:id).find_by!('author_id = ?', 2) end + test "find_by! doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) } + end + test "find_by! raises RecordNotFound if the record is missing" do assert_raises(ActiveRecord::RecordNotFound) do Post.scoped.find_by!("1 = 0") diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 377fde5c96..cef08cd99c 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -38,7 +38,9 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name t.text :settings, :null => true - t.text :preferences, :null => false, :default => "" + # MySQL does not allow default values for blobs. Fake it out with a + # big varchar below. + t.string :preferences, :null => false, :default => '', :limit => 1024 t.references :account end @@ -173,6 +175,7 @@ ActiveRecord::Schema.define do t.integer :client_of t.integer :rating, :default => 1 t.integer :account_id + t.string :description, :null => false, :default => "" end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 82921741b8..b02cefa600 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev* + * Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan* * Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo* diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index dbf0c25c5c..8f018dcbc6 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -22,20 +22,6 @@ #++ require 'securerandom' - -module ActiveSupport - class << self - attr_accessor :load_all_hooks - def on_load_all(&hook) load_all_hooks << hook end - def load_all!; load_all_hooks.each { |hook| hook.call } end - end - self.load_all_hooks = [] - - on_load_all do - [Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte] - end -end - require "active_support/dependencies/autoload" require "active_support/version" require "active_support/logger" @@ -44,6 +30,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :Dependencies autoload :DescendantsTracker autoload :FileUpdateChecker autoload :LogSubscriber diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 0e5aa5af10..020fa1a06d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -91,6 +91,17 @@ class DateTime change(:hour => 23, :min => 59, :sec => 59) end + # Returns a new DateTime representing the start of the hour (hh:00:00) + def beginning_of_hour + change(:min => 0) + end + alias :at_beginning_of_hour :beginning_of_hour + + # Returns a new DateTime representing the end of the hour (hh:59:59) + def end_of_hour + change(:min => 59, :sec => 59) + end + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0 # # Example: diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index fd1cda991e..501483498d 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/deep_dup' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' diff --git a/activesupport/lib/active_support/core_ext/hash/deep_dup.rb b/activesupport/lib/active_support/core_ext/hash/deep_dup.rb deleted file mode 100644 index 9ab179c566..0000000000 --- a/activesupport/lib/active_support/core_ext/hash/deep_dup.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Hash - # Returns a deep copy of hash. - def deep_dup - duplicate = self.dup - duplicate.each_pair do |k,v| - duplicate[k] = v.is_a?(Hash) ? v.deep_dup : v - end - duplicate - end -end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 822f766af7..2bf3d1f278 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -8,13 +8,13 @@ class Numeric # These methods use Time#advance for precise date calculations when using from_now, ago, etc. # as well as adding or subtracting their results from a Time object. For example: # - # # equivalent to Time.now.advance(:months => 1) + # # equivalent to Time.current.advance(:months => 1) # 1.month.from_now # - # # equivalent to Time.now.advance(:years => 2) + # # equivalent to Time.current.advance(:years => 2) # 2.years.from_now # - # # equivalent to Time.now.advance(:months => 4, :years => 5) + # # equivalent to Time.current.advance(:months => 4, :years => 5) # (4.months + 5.years).from_now # # While these methods provide precise calculation when used as in the examples above, care diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 9ad1e12699..ec2157221f 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/object/deep_dup' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/inclusion' diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb new file mode 100644 index 0000000000..2c4383ac94 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -0,0 +1,22 @@ +class Object + # Returns a deep copy of object if it's duplicable. + def deep_dup + duplicable? ? dup : self + end +end + +class Array + # Returns a deep copy of array. + def deep_dup + map { |it| it.deep_dup } + end +end + +class Hash + # Returns a deep copy of hash. + def deep_dup + each_with_object(dup) do |(key, value), hash| + hash[key.deep_dup] = value.deep_dup + end + end +end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 1434e186c3..a0f610d60c 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -257,6 +257,21 @@ class Time ) end + # Returns a new Time representing the start of the hour (x:00) + def beginning_of_hour + change(:min => 0) + end + alias :at_beginning_of_hour :beginning_of_hour + + # Returns a new Time representing the end of the hour, x:59:59.999999 (.999999999 in ruby1.9) + def end_of_hour + change( + :min => 59, + :sec => 59, + :usec => 999999.999 + ) + end + # Returns a new Time representing the start of the month (1st of the month, 0:00) def beginning_of_month #self - ((self.mday-1).days + self.seconds_since_midnight) diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 9a748dfa60..b20c980f36 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -62,8 +62,8 @@ module ActiveSupport #:nodoc: # Returns +true+ if _obj_ responds to the given method. Private methods are included in the search # only if the optional second parameter evaluates to +true+. - def respond_to?(method, include_private=false) - super || @wrapped_string.respond_to?(method, include_private) + def respond_to_missing?(method, include_private) + @wrapped_string.respond_to?(method, include_private) end # Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise. diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 538e41e0eb..60e6cd55ad 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -36,7 +36,7 @@ module ActiveSupport #:nodoc: end end - def respond_to?(name) + def respond_to_missing?(name, include_private) true end end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 9634b52ecf..bcd5d78b54 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -4,10 +4,6 @@ module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' autoload :TimeZone, 'active_support/values/time_zone' - - on_load_all do - [Duration, TimeWithZone, TimeZone] - end end require 'date' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 9d89e425fc..cd07c24257 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -313,10 +313,10 @@ module ActiveSupport end # Ensure proxy class responds to all methods that underlying time instance responds to. - def respond_to?(sym, include_priv = false) + def respond_to_missing?(sym, include_priv) # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime - return false if sym.to_s == 'acts_like_date?' - super || time.respond_to?(sym, include_priv) + return false if sym.to_sym == :acts_like_date? + time.respond_to?(sym, include_priv) end # Send the missing method to +time+ instance, and wrap result in a new TimeWithZone with the existing +time_zone+. diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index cd8cb5d18b..3da0825489 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -91,6 +91,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day end + def test_beginning_of_hour + assert_equal DateTime.civil(2005,2,4,19,0,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_hour + end + + def test_end_of_hour + assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour + end + def test_beginning_of_month assert_equal DateTime.civil(2005,2,1,0,0,0), DateTime.civil(2005,2,22,10,10,10).beginning_of_month end diff --git a/activesupport/test/core_ext/deep_dup_test.rb b/activesupport/test/core_ext/deep_dup_test.rb new file mode 100644 index 0000000000..91d558dbb5 --- /dev/null +++ b/activesupport/test/core_ext/deep_dup_test.rb @@ -0,0 +1,53 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class DeepDupTest < ActiveSupport::TestCase + + def test_array_deep_dup + array = [1, [2, 3]] + dup = array.deep_dup + dup[1][2] = 4 + assert_equal nil, array[1][2] + assert_equal 4, dup[1][2] + end + + def test_hash_deep_dup + hash = { :a => { :b => 'b' } } + dup = hash.deep_dup + dup[:a][:c] = 'c' + assert_equal nil, hash[:a][:c] + assert_equal 'c', dup[:a][:c] + end + + def test_array_deep_dup_with_hash_inside + array = [1, { :a => 2, :b => 3 } ] + dup = array.deep_dup + dup[1][:c] = 4 + assert_equal nil, array[1][:c] + assert_equal 4, dup[1][:c] + end + + def test_hash_deep_dup_with_array_inside + hash = { :a => [1, 2] } + dup = hash.deep_dup + dup[:a][2] = 'c' + assert_equal nil, hash[:a][2] + assert_equal 'c', dup[:a][2] + end + + def test_deep_dup_initialize + zero_hash = Hash.new 0 + hash = { :a => zero_hash } + dup = hash.deep_dup + assert_equal 0, dup[:a][44] + end + + def test_object_deep_dup + object = Object.new + dup = object.deep_dup + dup.instance_variable_set(:@a, 1) + assert !object.instance_variable_defined?(:@a) + assert dup.instance_variable_defined?(:@a) + end + +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 80b3c16328..1cd10eb6e2 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -363,21 +363,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end - def test_deep_dup - hash = { :a => { :b => 'b' } } - dup = hash.deep_dup - dup[:a][:c] = 'c' - assert_equal nil, hash[:a][:c] - assert_equal 'c', dup[:a][:c] - end - - def test_deep_dup_initialize - zero_hash = Hash.new 0 - hash = { :a => zero_hash } - dup = hash.deep_dup - assert_equal 0, dup[:a][44] - end - def test_store_on_indifferent_access hash = HashWithIndifferentAccess.new hash.store(:test1, 1) diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index cf1ec448c2..9c3389ba82 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -41,6 +41,18 @@ class RangeTest < ActiveSupport::TestCase assert((1..10).include?(1...10)) end + def test_should_compare_identical_inclusive + assert((1..10) === (1..10)) + end + + def test_should_compare_identical_exclusive + assert((1...10) === (1...10)) + end + + def test_should_compare_other_with_exlusive_end + assert((1..10) === (1...10)) + end + def test_exclusive_end_should_not_include_identical_with_inclusive_end assert !(1...10).include?(1..10) end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index c542acca68..4c1ed4b1ae 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -93,6 +93,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_beginning_of_hour + assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour + end + def test_beginning_of_month assert_equal Time.local(2005,2,1,0,0,0), Time.local(2005,2,22,10,10,10).beginning_of_month end @@ -127,6 +131,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2007,9,2,23,59,59,999999.999), Time.local(2007,9,02,0,0,0).end_of_week #sunday end + def test_end_of_hour + assert_equal Time.local(2005,2,4,19,59,59,999999.999), Time.local(2005,2,4,19,30,10).end_of_hour + end + def test_end_of_month assert_equal Time.local(2005,3,31,23,59,59,999999.999), Time.local(2005,3,20,10,10,10).end_of_month assert_equal Time.local(2005,2,28,23,59,59,999999.999), Time.local(2005,2,20,10,10,10).end_of_month diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 7cf3842a16..b62337e31b 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -191,7 +191,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end end - def future_with_time_current_as_time_with_zone + def test_future_with_time_current_as_time_with_zone twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) Time.stubs(:current).returns(twz) assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? @@ -450,6 +450,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_ruby_19_weekday_name_query_methods %w(sunday? monday? tuesday? wednesday? thursday? friday? saturday?).each do |name| assert_respond_to @twz, name + assert_equal @twz.send(name), @twz.method(name).call end end @@ -482,36 +483,50 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.advance(:seconds => 30).inspect end - def beginning_of_year + def test_beginning_of_year assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 01 Jan 1999 00:00:00 EST -05:00", @twz.beginning_of_year.inspect end - def end_of_year + def test_end_of_year assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_year.inspect end - def beginning_of_month + def test_beginning_of_month assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect - assert_equal "Fri, 01 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_month.inspect + assert_equal "Wed, 01 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_month.inspect end - def end_of_month + def test_end_of_month assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_month.inspect end - def beginning_of_day + def test_beginning_of_day assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_day.inspect end - def end_of_day + def test_end_of_day assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_day.inspect end + def test_beginning_of_hour + utc = Time.utc(2000, 1, 1, 0, 30) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:00 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", twz.beginning_of_hour.inspect + end + + def test_end_of_hour + utc = Time.utc(2000, 1, 1, 0, 30) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:00 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:59:59 EST -05:00", twz.end_of_hour.inspect + end + def test_since assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 90aa13b3e6..a8d69d0ec3 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -458,6 +458,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert !''.mb_chars.respond_to?(:undefined_method) # Not defined end + def test_method_works_for_proxyed_methods + assert_equal 'll', 'hello'.mb_chars.method(:slice).call(2..3) # Defined on Chars + chars = 'hello'.mb_chars + assert_equal 'Hello', chars.method(:capitalize!).call # Defined on Chars + assert_equal 'Hello', chars + assert_equal 'jello', 'hello'.mb_chars.method(:gsub).call(/h/, 'j') # Defined on String + assert_raise(NameError){ ''.mb_chars.method(:undefined_method) } # Not defined + end + def test_acts_like_string assert 'Bambi'.mb_chars.acts_like_string? end diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index 3526c7a366..f60f9a58e3 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -77,4 +77,12 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert copy.kind_of?(original.class) assert_not_equal copy.object_id, original.object_id end + + def test_introspection + a = ActiveSupport::OrderedOptions.new + assert a.respond_to?(:blah) + assert a.respond_to?(:blah=) + assert_equal 42, a.method(:blah=).call(42) + assert_equal 42, a.method(:blah).call + end end diff --git a/guides/rails_guides/textile_extensions.rb b/guides/rails_guides/textile_extensions.rb index 4677fae504..0a002a785f 100644 --- a/guides/rails_guides/textile_extensions.rb +++ b/guides/rails_guides/textile_extensions.rb @@ -1,5 +1,11 @@ require 'active_support/core_ext/object/inclusion' +module RedCloth::Formatters::HTML + def emdash(opts) + "--" + end +end + module RailsGuides module TextileExtensions def notestuff(body) diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile index f9dbaa1125..a9cb424eaa 100644 --- a/guides/source/active_record_querying.textile +++ b/guides/source/active_record_querying.textile @@ -99,9 +99,28 @@ SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 <tt>Model.find(primary_key)</tt> will raise an +ActiveRecord::RecordNotFound+ exception if no matching record is found. +h5. +take+ + +<tt>Model.take</tt> retrieves a record without any implicit ordering. For example: + +<ruby> +client = Client.take +# => #<Client id: 1, first_name: "Lifo"> +</ruby> + +The SQL equivalent of the above is: + +<sql> +SELECT * FROM clients LIMIT 1 +</sql> + +<tt>Model.take</tt> returns +nil+ if no record is found and no exception will be raised. + +TIP: The retrieved record may vary depending on the database engine. + h5. +first+ -<tt>Model.first</tt> finds the first record matched by the supplied options, if any. For example: +<tt>Model.first</tt> finds the first record ordered by the primary key. For example: <ruby> client = Client.first @@ -111,14 +130,14 @@ client = Client.first The SQL equivalent of the above is: <sql> -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 </sql> -<tt>Model.first</tt> returns +nil+ if no matching record is found. No exception will be raised. +<tt>Model.first</tt> returns +nil+ if no matching record is found and no exception will be raised. h5. +last+ -<tt>Model.last</tt> finds the last record matched by the supplied options. For example: +<tt>Model.last</tt> finds the last record ordered by the primary key. For example: <ruby> client = Client.last @@ -131,7 +150,7 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 </sql> -<tt>Model.last</tt> returns +nil+ if no matching record is found. No exception will be raised. +<tt>Model.last</tt> returns +nil+ if no matching record is found and no exception will be raised. h5. +find_by+ @@ -148,12 +167,29 @@ Client.find_by first_name: 'Jon' It is equivalent to writing: <ruby> -Client.where(first_name: 'Lifo').first +Client.where(first_name: 'Lifo').take </ruby> +h5(#take_1). +take!+ + +<tt>Model.take!</tt> retrieves a record without any implicit ordering. For example: + +<ruby> +client = Client.take! +# => #<Client id: 1, first_name: "Lifo"> +</ruby> + +The SQL equivalent of the above is: + +<sql> +SELECT * FROM clients LIMIT 1 +</sql> + +<tt>Model.take!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. + h5(#first_1). +first!+ -<tt>Model.first!</tt> finds the first record. For example: +<tt>Model.first!</tt> finds the first record ordered by the primary key. For example: <ruby> client = Client.first! @@ -163,14 +199,14 @@ client = Client.first! The SQL equivalent of the above is: <sql> -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 </sql> -<tt>Model.first!</tt> raises +RecordNotFound+ if no matching record is found. +<tt>Model.first!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. h5(#last_1). +last!+ -<tt>Model.last!</tt> finds the last record. For example: +<tt>Model.last!</tt> finds the last record ordered by the primary key. For example: <ruby> client = Client.last! @@ -183,24 +219,24 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 </sql> -<tt>Model.last!</tt> raises +RecordNotFound+ if no matching record is found. +<tt>Model.last!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. h5(#find_by_1). +find_by!+ -<tt>Model.find_by!</tt> finds the first record matching some conditions. It raises +RecordNotFound+ if no matching record is found. For example: +<tt>Model.find_by!</tt> finds the first record matching some conditions. It raises +ActiveRecord::RecordNotFound+ if no matching record is found. For example: <ruby> Client.find_by! first_name: 'Lifo' # => #<Client id: 1, first_name: "Lifo"> Client.find_by! first_name: 'Jon' -# => RecordNotFound +# => ActiveRecord::RecordNotFound </ruby> It is equivalent to writing: <ruby> -Client.where(first_name: 'Lifo').first! +Client.where(first_name: 'Lifo').take! </ruby> h4. Retrieving Multiple Objects diff --git a/guides/source/active_support_core_extensions.textile b/guides/source/active_support_core_extensions.textile index 43dc0c12cf..8045316e98 100644 --- a/guides/source/active_support_core_extensions.textile +++ b/guides/source/active_support_core_extensions.textile @@ -154,6 +154,51 @@ WARNING. Any class can disallow duplication removing +dup+ and +clone+ or raisin NOTE: Defined in +active_support/core_ext/object/duplicable.rb+. +h4. +deep_dup+ + +The +deep_dup+ method returns deep copy of given object. Normally, when you +dup+ an object that contains other objects, ruby does not +dup+ them. If you have array with a string, for example, it will look like this: + +<ruby> +array = ['string'] +duplicate = array.dup + +duplicate.push 'another-string' + +# object was duplicated, element added only to duplicate +array #=> ['string'] +duplicate #=> ['string', 'another-string'] + +duplicate.first.gsub!('string', 'foo') + +# first element was not duplicated, it will be changed for both arrays +array #=> ['foo'] +duplicate #=> ['foo', 'another-string'] +</ruby> + +As you can see, after duplicating +Array+ instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since +dup+ does not make deep copy, the string inside array is still the same object. + +If you need a deep copy of an object, you should use +deep_dup+ in such situation: + +<ruby> +array = ['string'] +duplicate = array.deep_dup + +duplicate.first.gsub!('string', 'foo') + +array #=> ['string'] +duplicate #=> ['foo'] +</ruby> + +If object is not duplicable +deep_dup+ will just return this object: + +<ruby> +number = 1 +dup = number.deep_dup +number.object_id == dup.object_id # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/object/deep_dup.rb+. + h4. +try+ Sometimes you want to call a method provided the receiver object is not +nil+, which is something you usually check first. +try+ is like +Object#send+ except that it returns +nil+ if sent to +nil+. @@ -184,7 +229,7 @@ You can evaluate code in the context of any object's singleton class using +clas <ruby> class Proc def bind(object) - block, time = self, Time.now + block, time = self, Time.current object.class_eval do method_name = "__bind_#{time.to_i}_#{time.usec}" define_method(method_name, &block) @@ -1034,49 +1079,6 @@ A model may find it useful to set +:instance_accessor+ to +false+ as a way to pr NOTE: Defined in +active_support/core_ext/class/attribute_accessors.rb+. -h4. Class Inheritable Attributes - -WARNING: Class Inheritable Attributes are deprecated. It's recommended that you use +Class#class_attribute+ instead. - -Class variables are shared down the inheritance tree. Class instance variables are not shared, but they are not inherited either. The macros +class_inheritable_reader+, +class_inheritable_writer+, and +class_inheritable_accessor+ provide accessors for class-level data which is inherited but not shared with children: - -<ruby> -module ActionController - class Base - # FIXME: REVISE/SIMPLIFY THIS COMMENT. - # The value of allow_forgery_protection is inherited, - # but its value in a particular class does not affect - # the value in the rest of the controllers hierarchy. - class_inheritable_accessor :allow_forgery_protection - end -end -</ruby> - -They accomplish this with class instance variables and cloning on subclassing, there are no class variables involved. Cloning is performed with +dup+ as long as the value is duplicable. - -There are some variants specialised in arrays and hashes: - -<ruby> -class_inheritable_array -class_inheritable_hash -</ruby> - -Those writers take any inherited array or hash into account and extend them rather than overwrite them. - -As with vanilla class attribute accessors these macros create convenience instance methods for reading and writing. The generation of the writer instance method can be prevented setting +:instance_writer+ to +false+ (not any false value, but exactly +false+): - -<ruby> -module ActiveRecord - class Base - class_inheritable_accessor :default_scoping, :instance_writer => false - end -end -</ruby> - -Since values are copied when a subclass is defined, if the base class changes the attribute after that, the subclass does not see the new value. That's the point. - -NOTE: Defined in +active_support/core_ext/class/inheritable_attributes.rb+. - h4. Subclasses & Descendants h5. +subclasses+ @@ -1255,9 +1257,14 @@ Pass a +:separator+ to truncate the string at a natural break: # => "Oh dear! Oh..." </ruby> -In the above example "dear" gets cut first, but then +:separator+ prevents it. +The option +:separator+ can be a regexp: -WARNING: The option +:separator+ can't be a regexp. +<ruby> +"Oh dear! Oh dear! I shall be late!".truncate(18, :separator => /\s/) +# => "Oh dear! Oh..." +</ruby> + +In above examples "dear" gets cut first, but then +:separator+ prevents it. NOTE: Defined in +active_support/core_ext/string/filters.rb+. @@ -1810,6 +1817,43 @@ Singular forms are aliased so you are able to say: NOTE: Defined in +active_support/core_ext/numeric/bytes.rb+. +h4. Time + +Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. + +These methods use Time#advance for precise date calculations when using from_now, ago, etc. +as well as adding or subtracting their results from a Time object. For example: + +<ruby> +# equivalent to Time.current.advance(:months => 1) +1.month.from_now + +# equivalent to Time.current.advance(:years => 2) +2.years.from_now + +# equivalent to Time.current.advance(:months => 4, :years => 5) +(4.months + 5.years).from_now +</ruby> + +While these methods provide precise calculation when used as in the examples above, care +should be taken to note that this is not true if the result of `months', `years', etc is +converted before use: + +<ruby> +# equivalent to 30.days.to_i.from_now +1.month.to_i.from_now + +# equivalent to 365.25.days.to_f.from_now +1.year.to_f.from_now +</ruby> + +In such cases, Ruby's core +Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and +Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision +date and time arithmetic. + +NOTE: Defined in +active_support/core_ext/numeric/time.rb+. + h3. Extensions to +Integer+ h4. +multiple_of?+ @@ -2103,20 +2147,20 @@ To do so it sends +to_xml+ to every item in turn, and collects the results under By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with <tt>is_a?</tt>) and they are not hashes. In the example above that's "contributors". -If there's any element that does not belong to the type of the first one the root node becomes "records": +If there's any element that does not belong to the type of the first one the root node becomes "objects": <ruby> [Contributor.first, Commit.first].to_xml # => # <?xml version="1.0" encoding="UTF-8"?> -# <records type="array"> -# <record> +# <objects type="array"> +# <object> # <id type="integer">4583</id> # <name>Aaron Batalion</name> # <rank type="integer">53</rank> # <url-id>aaron-batalion</url-id> -# </record> -# <record> +# </object> +# <object> # <author>Joshua Peek</author> # <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp> # <branch>origin/master</branch> @@ -2127,30 +2171,30 @@ If there's any element that does not belong to the type of the first one the roo # <imported-from-svn type="boolean">false</imported-from-svn> # <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message> # <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1> -# </record> -# </records> +# </object> +# </objects> </ruby> -If the receiver is an array of hashes the root element is by default also "records": +If the receiver is an array of hashes the root element is by default also "objects": <ruby> [{:a => 1, :b => 2}, {:c => 3}].to_xml # => # <?xml version="1.0" encoding="UTF-8"?> -# <records type="array"> -# <record> +# <objects type="array"> +# <object> # <b type="integer">2</b> # <a type="integer">1</a> -# </record> -# <record> +# </object> +# <object> # <c type="integer">3</c> -# </record> -# </records> +# </object> +# </objects> </ruby> WARNING. If the collection is empty the root element is by default "nil-classes". That's a gotcha, for example the root element of the list of contributors above would not be "contributors" if the collection was empty, but "nil-classes". You may use the <tt>:root</tt> option to ensure a consistent root element. -The name of children nodes is by default the name of the root node singularized. In the examples above we've seen "contributor" and "record". The option <tt>:children</tt> allows you to set these node names. +The name of children nodes is by default the name of the root node singularized. In the examples above we've seen "contributor" and "object". The option <tt>:children</tt> allows you to set these node names. The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can configure your own builder via the <tt>:builder</tt> option. The method also accepts options like <tt>:dasherize</tt> and friends, they are forwarded to the builder: @@ -2217,6 +2261,19 @@ Thus, in this case the behavior is different for +nil+, and the differences with NOTE: Defined in +active_support/core_ext/array/wrap.rb+. +h4. Duplicating + +The method +Array.deep_dup+ duplicates itself and all objects inside recursively with ActiveSupport method +Object#deep_dup+. It works like +Array#map+ with sending +deep_dup+ method to each object inside. + +<ruby> +array = [1, [2, 3]] +dup = array.deep_dup +dup[1][2] = 4 +array[1][2] == nil # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/array/deep_dup.rb+. + h4. Grouping h5. +in_groups_of(number, fill_with = nil)+ @@ -2423,6 +2480,23 @@ The method +deep_merge!+ performs a deep merge in place. NOTE: Defined in +active_support/core_ext/hash/deep_merge.rb+. +h4. Deep duplicating + +The method +Hash.deep_dup+ duplicates itself and all keys and values inside recursively with ActiveSupport method +Object#deep_dup+. It works like +Enumerator#each_with_object+ with sending +deep_dup+ method to each pair inside. + +<ruby> +hash = { :a => 1, :b => { :c => 2, :d => [3, 4] } } + +dup = hash.deep_dup +dup[:b][:e] = 5 +dup[:b][:d] << 5 + +hash[:b][:e] == nil # => true +hash[:b][:d] == [3, 4] # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/hash/deep_dup.rb+. + h4. Diffing The method +diff+ returns a hash that represents a diff of the receiver and the argument with the following logic: @@ -2707,22 +2781,25 @@ NOTE: Defined in +active_support/core_ext/range/blockless_step.rb+. h4. +include?+ -The method +Range#include?+ says whether some value falls between the ends of a given instance: +The methods +Range#include?+ and +Range#===+ say whether some value falls between the ends of a given instance: <ruby> (2..3).include?(Math::E) # => true </ruby> -Active Support extends this method so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves: +Active Support extends these methods so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves: <ruby> (1..10).include?(3..7) # => true (1..10).include?(0..7) # => false (1..10).include?(3..11) # => false (1...9).include?(3..9) # => false -</ruby> -WARNING: The original +Range#include?+ is still the one aliased to +Range#===+. +(1..10) === (3..7) # => true +(1..10) === (0..7) # => false +(1..10) === (3..11) # => false +(1...9) === (3..9) # => false +</ruby> NOTE: Defined in +active_support/core_ext/range/include_range.rb+. diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index 5bfb45f778..c4e54348d4 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -76,6 +76,17 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.consider_all_requests_local+ is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the +Rails::Info+ controller will show the application runtime context in +/rails/info/properties+. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement +local_request?+ in controllers to specify which requests should provide debugging information on errors. +* +config.console+ allows you to set class that will be used as console you run +rails console+. It's best to run it in +console+ block: + +<ruby> +console do + # this block is called only when running console, + # so we can safely require pry here + require "pry" + config.console = Pry +end +</ruby> + * +config.dependency_loading+ is a flag that allows you to disable constant autoloading setting it to false. It only has effect if +config.cache_classes+ is true, which it is by default in production mode. This flag is set to false by +config.threadsafe!+. * +config.eager_load_paths+ accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the +app+ directory of the application. @@ -100,6 +111,10 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled. +* +config.queue+ configures a different queue implementation for the application. Defaults to +Rails::Queueing::Queue+. Note that, if the default queue is changed, the default +queue_consumer+ is not going to be initialized, it is up to the new queue implementation to handle starting and shutting down its own consumer(s). + +* +config.queue_consumer+ configures a different consumer implementation for the default queue. Defaults to +Rails::Queueing::ThreadedConsumer+. + * +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If +config.cache_classes+ is true, this option is ignored. * +config.secret_token+ used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get +config.secret_token+ initialized to a random key in +config/initializers/secret_token.rb+. @@ -122,17 +137,6 @@ WARNING: Threadsafe operation is incompatible with the normal workings of develo * +config.whiny_nils+ enables or disables warnings when a certain set of methods are invoked on +nil+ and it does not respond to them. Defaults to true in development and test environments. -* +config.console+ allows you to set class that will be used as console you run +rails console+. It's best to run it in +console+ block: - -<ruby> -console do - # this block is called only when running console, - # so we can safely require pry here - require "pry" - config.console = Pry -end -</ruby> - h4. Configuring Assets Rails 3.1, by default, is set up to use the +sprockets+ gem to manage assets within an application. This gem concatenates and compresses assets in order to make serving them much less painful. diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 5439459b42..74805b2754 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -13,7 +13,7 @@ Ruby on Rails Guides and <%= link_to 'Free Kindle Reading Apps', 'http://www.amazon.com/gp/kindle/kcp' %> for the iPad, iPhone, Mac, Android, etc. Download them from <%= link_to 'here', @mobi %>. </dd> - <dd class="work-in-progress">Guides marked with this icon are currently being worked on. While they might still be useful to you, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections to the author.</dd> + <dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd> </dl> </div> <% end %> diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index bc34ced283..b7c042cee3 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* Add `config.queue_consumer` to allow the default consumer to be configurable. *Carlos Antonio da Silva* + +* Add Rails.queue as an interface with a default implementation that consumes jobs in a separate thread. *Yehuda Katz* + * Remove Rack::SSL in favour of ActionDispatch::SSL. *Rafael Mendonça França* * Remove Active Resource from Rails framework. *Prem Sichangrist* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index c7b19c964a..c4edbae55b 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -66,7 +66,7 @@ module Rails end end - attr_accessor :assets, :sandbox + attr_accessor :assets, :sandbox, :queue_consumer alias_method :sandbox?, :sandbox attr_reader :reloaders attr_writer :queue diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 25bb680f69..a2e5dece16 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -8,10 +8,11 @@ module Rails attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :autoflush_log, :cache_classes, :cache_store, :consider_all_requests_local, :console, :dependency_loading, :exceptions_app, :file_watcher, :filter_parameters, - :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :preload_frameworks, - :railties_order, :relative_url_root, :secret_token, + :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, + :preload_frameworks, :railties_order, :relative_url_root, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump, :queue + :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump, + :queue, :queue_consumer attr_writer :log_level attr_reader :encoding @@ -44,6 +45,7 @@ module Rails @log_formatter = ActiveSupport::Logger::SimpleFormatter.new @use_schema_cache_dump = true @queue = Rails::Queueing::Queue + @queue_consumer = Rails::Queueing::ThreadedConsumer @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 6a24e01f29..84f2601f28 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -96,8 +96,8 @@ module Rails initializer :activate_queue_consumer do |app| if config.queue == Rails::Queueing::Queue - consumer = Rails::Queueing::ThreadedConsumer.start(app.queue) - at_exit { consumer.shutdown } + app.queue_consumer = config.queue_consumer.start(app.queue) + at_exit { app.queue_consumer.shutdown } end end end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 82cdd6053b..7f473c237c 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -57,8 +57,7 @@ when 'server' when 'dbconsole' require 'rails/commands/dbconsole' - require APP_PATH - Rails::DBConsole.start(Rails.application) + Rails::DBConsole.start when 'application', 'runner' require "rails/commands/#{command}" diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 25a8caeec3..aaba47117f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -5,15 +5,37 @@ require 'rbconfig' module Rails class DBConsole - attr_reader :arguments + attr_reader :arguments, :config - def self.start(app) - new(app).start + def self.start + new(config).start end - def initialize(app, arguments = ARGV) - @app = app - @arguments = arguments + def self.config + config = begin + YAML.load(ERB.new(IO.read("config/database.yml")).result) + rescue SyntaxError, StandardError + require APP_PATH + Rails.application.config.database_configuration + end + + unless config[env] + abort "No database is configured for the environment '#{env}'" + end + + config[env] + end + + def self.env + if Rails.respond_to?(:env) + Rails.env + else + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + end + + def initialize(config, arguments = ARGV) + @config, @arguments = config, arguments end def start @@ -38,10 +60,6 @@ module Rails abort opt.to_s unless (0..1).include?(arguments.size) end - unless config = @app.config.database_configuration[Rails.env] - abort "No database is configured for the environment '#{Rails.env}'" - end - case config["adapter"] when /^mysql/ diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 493eacdc5a..3d66019e5e 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -1,6 +1,6 @@ require 'active_support/deprecation' require 'active_support/ordered_options' -require 'active_support/core_ext/hash/deep_dup' +require 'active_support/core_ext/object' require 'rails/paths' require 'rails/rack' diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index b71119af77..ffbc0b4bd6 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -34,6 +34,10 @@ The common rails commands available for engines are: destroy Undo code generated with "generate" (short-cut alias: "d") All commands can be run with -h for more information. + +If you want to run any commands that need to be run in context +of the application, like `rails server` or `rails console`, +you should do it from application's directory (typically test/dummy). EOT exit(1) end diff --git a/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt b/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt new file mode 100644 index 0000000000..1c49e5c70a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt @@ -0,0 +1,9 @@ +# See more about this file at: http://humanstxt.org/ +# For format suggestions, see: http://humanstxt.org/Standard.html +/* TEAM */ + <%= ENV['USER'].titlecase %> + +/* APP */ + Name: <%= app_const_base %> + Date Created: <%= Date.today.strftime("%B %d, %Y") %> + Software: Ruby on Rails diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb index 2e187b8555..b4bc7fcd18 100644 --- a/railties/lib/rails/queueing.rb +++ b/railties/lib/rails/queueing.rb @@ -16,13 +16,13 @@ module Rails # Jobs are run in a separate thread to catch mistakes where code # assumes that the job is run in the same thread. class TestQueue < ::Queue - # Get a list of the jobs off this queue. This method may not be + # Get a list of the jobs off this queue. This method may not be # available on production queues. def jobs @que.dup end - # Drain the queue, running all jobs in a different thread. This method + # Drain the queue, running all jobs in a different thread. This method # may not be available on production queues. def drain # run the jobs in a separate thread so assumptions of synchronous @@ -53,7 +53,7 @@ module Rails begin job.run rescue Exception => e - Rails.logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" + handle_exception e end end end @@ -64,6 +64,10 @@ module Rails @queue.push nil @thread.join end + + def handle_exception(e) + Rails.logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" + end end end end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 29ebdc6511..dfcf5aa27d 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -1,3 +1,5 @@ +ENV["RAILS_ENV"] ||= "test" + require File.expand_path("../../../load_paths", __FILE__) require 'stringio' diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb index 71b0c2bc38..da8bdeed52 100644 --- a/railties/test/application/queue_test.rb +++ b/railties/test/application/queue_test.rb @@ -63,7 +63,7 @@ module ApplicationTests test "in test mode, the queue can be observed" do app("test") - job = Class.new(Struct.new(:id)) do + job = Struct.new(:id) do def run end end @@ -79,7 +79,7 @@ module ApplicationTests assert_equal jobs, Rails.queue.jobs end - test "a custom queue implementation can be provided" do + def setup_custom_queue add_to_env_config "production", <<-RUBY require "my_queue" config.queue = MyQueue @@ -94,10 +94,14 @@ module ApplicationTests RUBY app("production") + end + + test "a custom queue implementation can be provided" do + setup_custom_queue assert_kind_of MyQueue, Rails.queue - job = Class.new(Struct.new(:id, :ran)) do + job = Struct.new(:id, :ran) do def run self.ran = true end @@ -108,5 +112,34 @@ module ApplicationTests assert_equal true, job1.ran end + + test "a custom consumer implementation can be provided" do + add_to_env_config "production", <<-RUBY + require "my_queue_consumer" + config.queue_consumer = MyQueueConsumer + RUBY + + app_file "lib/my_queue_consumer.rb", <<-RUBY + class MyQueueConsumer < Rails::Queueing::ThreadedConsumer + attr_reader :started + + def start + @started = true + self + end + end + RUBY + + app("production") + + assert_kind_of MyQueueConsumer, Rails.application.queue_consumer + assert Rails.application.queue_consumer.started + end + + test "default consumer is not used with custom queue implementation" do + setup_custom_queue + + assert_nil Rails.application.queue_consumer + end end end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 0bf417c014..85a7edfacd 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -3,42 +3,73 @@ require 'rails/commands/dbconsole' class Rails::DBConsoleTest < ActiveSupport::TestCase def teardown - %w[PGUSER PGHOST PGPORT PGPASSWORD'].each{|key| ENV.delete(key)} + %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)} end - def test_no_database_configured - start [], false + def test_config + Rails::DBConsole.const_set(:APP_PATH, "erb") + + app_config({}) + capture_abort { Rails::DBConsole.config } assert aborted assert_match /No database is configured for the environment '\w+'/, output + + app_config(test: "with_init") + assert_equal Rails::DBConsole.config, "with_init" + + app_db_file("test:\n without_init") + assert_equal Rails::DBConsole.config, "without_init" + + app_db_file("test:\n <%= Rails.something_app_specific %>") + assert_equal Rails::DBConsole.config, "with_init" + + app_db_file("test:\n\ninvalid") + assert_equal Rails::DBConsole.config, "with_init" + end + + def test_env + assert_equal Rails::DBConsole.env, "test" + + Rails.stubs(:respond_to?).with(:env).returns(false) + assert_equal Rails::DBConsole.env, "test" + + ENV['RAILS_ENV'] = nil + ENV['RACK_ENV'] = "rack_env" + assert_equal Rails::DBConsole.env, "rack_env" + + ENV['RAILS_ENV'] = "rails_env" + assert_equal Rails::DBConsole.env, "rails_env" + ensure + ENV['RAILS_ENV'] = "test" end def test_mysql dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db') - start [], {adapter: 'mysql', database: 'db'} + start(adapter: 'mysql', database: 'db') assert !aborted end def test_mysql_full dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db') - start [], {adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8'} + start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') assert !aborted end def test_mysql_include_password dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db') - start ['-p'], {adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'} + start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) assert !aborted end def test_postgresql dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') - start [], {adapter: 'postgresql', database: 'db'} + start(adapter: 'postgresql', database: 'db') assert !aborted end def test_postgresql_full dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') - start [], {adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432} + start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432) assert !aborted assert_equal 'user', ENV['PGUSER'] assert_equal 'host', ENV['PGHOST'] @@ -48,7 +79,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_postgresql_include_password dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') - start ['-p'], {adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'} + start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p']) assert !aborted assert_equal 'user', ENV['PGUSER'] assert_equal 'q1w2e3', ENV['PGPASSWORD'] @@ -56,42 +87,42 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_sqlite dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db') - start [], {adapter: 'sqlite', database: 'db'} + start(adapter: 'sqlite', database: 'db') assert !aborted end def test_sqlite3 dbconsole.expects(:find_cmd_and_exec).with('sqlite3', 'db') - start [], {adapter: 'sqlite3', database: 'db'} + start(adapter: 'sqlite3', database: 'db') assert !aborted end def test_sqlite3_mode dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', 'db') - start ['--mode', 'html'], {adapter: 'sqlite3', database: 'db'} + start({adapter: 'sqlite3', database: 'db'}, ['--mode', 'html']) assert !aborted end def test_sqlite3_header dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', 'db') - start ['--header'], {adapter: 'sqlite3', database: 'db'} + start({adapter: 'sqlite3', database: 'db'}, ['--header']) assert !aborted end def test_oracle dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db') - start [], {adapter: 'oracle', database: 'db', username: 'user', password: 'secret'} + start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret') assert !aborted end def test_oracle_include_password dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db') - start ['-p'], {adapter: 'oracle', database: 'db', username: 'user', password: 'secret'} + start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p']) assert !aborted end def test_unknown_command_line_client - start [], {adapter: 'unknown', database: 'db'} + start(adapter: 'unknown', database: 'db') assert aborted assert_match /Unknown command-line client for db/, output end @@ -100,29 +131,30 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase attr_reader :aborted, :output def dbconsole - @dbconsole ||= Rails::DBConsole.new(app) + @dbconsole ||= Rails::DBConsole.new(nil) end - def start(argv = [], database_configuration = {}) - dbconsole.stubs(arguments: argv) - app.config.stubs(database_configuration: { - Rails.env => database_configuration ? database_configuration.stringify_keys : database_configuration - }) + def start(config = {}, argv = []) + dbconsole.stubs(config: config.stringify_keys, arguments: argv) + capture_abort { dbconsole.start } + end + def capture_abort @aborted = false - @output = capture(:stderr) do + @output = capture(:stderr) do begin - dbconsole.start + yield rescue SystemExit @aborted = true end end end - def app - @app ||= begin - config = mock("config") - stub("app", config: config) - end + def app_db_file(result) + IO.stubs(:read).with("config/database.yml").returns(result) + end + + def app_config(result) + Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys) end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index d13dc8d4ac..a0c308a3b8 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -383,6 +383,12 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/run bundle install/, output) end + def test_humans_txt_file + date = Date.today.strftime("%B %d, %Y") + run_generator [File.join(destination_root, 'things-43')] + assert_file "things-43/public/humans.txt", /Name: Things43/, /Software: Ruby on Rails/, /Date Created: #{date}/ + end + protected def action(*args, &block) diff --git a/railties/test/queueing/threaded_consumer_test.rb b/railties/test/queueing/threaded_consumer_test.rb index 559de2a82d..c34a966d6e 100644 --- a/railties/test/queueing/threaded_consumer_test.rb +++ b/railties/test/queueing/threaded_consumer_test.rb @@ -78,4 +78,23 @@ class TestThreadConsumer < ActiveSupport::TestCase assert_equal 1, logger.logged(:error).size assert_match(/Job Error: RuntimeError: Error!/, logger.logged(:error).last) end + + test "test overriding exception handling" do + @consumer.shutdown + @consumer = Class.new(Rails::Queueing::ThreadedConsumer) do + attr_reader :last_error + def handle_exception(e) + @last_error = e.message + end + end.start(@queue) + + job = Job.new(1) do + raise "RuntimeError: Error!" + end + + @queue.push job + sleep 0.1 + + assert_equal "RuntimeError: Error!", @consumer.last_error + end end |