diff options
71 files changed, 1141 insertions, 470 deletions
@@ -5,6 +5,10 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem 'rake', '>= 10.3' +# Active Support depends on a prerelease concurrent-ruby 1.0.0, so track +# latest master as it approaches release. +gem 'concurrent-ruby', '~> 1.0.0.pre3', github: 'ruby-concurrency/concurrent-ruby' + # Active Job depends on the URI::GID::MissingModelIDError, which isn't released yet. gem 'globalid', github: 'rails/globalid', branch: 'master' gem 'rack', github: 'rack/rack', branch: 'master' @@ -21,8 +25,8 @@ gem 'turbolinks', github: 'rails/turbolinks', branch: 'master' gem 'arel', github: 'rails/arel', branch: 'master' gem 'mail', github: 'mikel/mail', branch: 'master' -gem 'sprockets', github: 'rails/sprockets', branch: 'master' -gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master' +gem 'sprockets', '~> 4.0', github: 'rails/sprockets', branch: 'master' +gem 'sprockets-rails', '~> 3.0.0.beta3', github: 'rails/sprockets-rails', branch: 'master' gem 'sass-rails', github: 'rails/sass-rails', branch: 'master' # require: false so bcrypt is loaded only when has_secure_password is used. @@ -33,7 +37,9 @@ gem 'bcrypt', '~> 3.1.10', require: false # This needs to be with require false to avoid # it being automatically loaded by sprockets gem 'uglifier', '>= 1.3.0', require: false -gem 'sass', '>= 3.3', require: false + +# Track stable branch of sass because it doesn't have circular require warnings +gem 'sass', github: 'sass/sass', branch: 'stable', require: false group :doc do gem 'sdoc', '~> 0.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 563a9f56be..7d6db0474e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,10 +73,10 @@ GIT GIT remote: git://github.com/rails/sprockets-rails.git - revision: 600f981fe79ff2d4179baf84bd18fac5acb58b5e + revision: 77098c5acd9f27613875097ce6587aff9d871d7f branch: master specs: - sprockets-rails (3.0.0.beta2) + sprockets-rails (3.0.0.beta3) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -97,6 +97,19 @@ GIT turbolinks (3.0.0) coffee-rails +GIT + remote: git://github.com/ruby-concurrency/concurrent-ruby.git + revision: 10ceb96c146d2937b66e7a444445e5ab9edbeb7c + specs: + concurrent-ruby (1.0.0.pre3) + +GIT + remote: git://github.com/sass/sass.git + revision: 4ef8e3167985ace91b2105916756bd93c5d7bba6 + branch: stable + specs: + sass (3.4.18) + PATH remote: . specs: @@ -130,7 +143,7 @@ PATH activesupport (= 5.0.0.alpha) arel (= 7.0.0.alpha) activesupport (5.0.0.alpha) - concurrent-ruby (~> 1.0.0.pre2, < 2.0.0) + concurrent-ruby (~> 1.0.0.pre3, < 2.0.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) method_source @@ -179,7 +192,6 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) - concurrent-ruby (1.0.0.pre2) connection_pool (2.2.0) dalli (2.7.4) dante (0.2.0) @@ -251,7 +263,6 @@ GEM resque (~> 1.25) rufus-scheduler (~> 3.0) rufus-scheduler (3.1.4) - sass (3.4.18) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) @@ -309,6 +320,7 @@ DEPENDENCIES benchmark-ips byebug coffee-rails (~> 4.1.0) + concurrent-ruby (~> 1.0.0.pre3)! dalli (>= 2.2.1) delayed_job delayed_job_active_record @@ -336,14 +348,14 @@ DEPENDENCIES redcarpet (~> 3.2.3) resque resque-scheduler - sass (>= 3.3) + sass! sass-rails! sdoc (~> 0.4.0) sequel sidekiq sneakers - sprockets! - sprockets-rails! + sprockets (~> 4.0)! + sprockets-rails (~> 3.0.0.beta3)! sqlite3 (~> 1.3.6) stackprof sucker_punch diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 85d9c3be00..f6f63f1f32 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -161,6 +161,26 @@ module ActionDispatch # :nodoc: def set_header(key, v); headers[key] = v; end def delete_header(key); headers.delete key; end + # Add a header that may have multiple values. + # + # Example: + # response.add_header 'Vary', 'Accept' + # response.add_header 'Vary', 'Accept-Encoding' + # response.add_header 'Vary', 'Cookie' + # + # assert_equal 'Accept,Accept-Encoding,Cookie', response.get_header 'Vary' + # + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + def add_header(key, v) + if v.nil? + get_header key + elsif have_header? key + set_header key, "#{get_header key},#{v}" + else + set_header key, v + end + end + def await_commit synchronize do @cv.wait_until { @committed } diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 7ed77352ae..2889acaeb8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -396,7 +396,9 @@ module ActionDispatch end def write(headers) - headers[HTTP_HEADER] = make_set_cookie_header headers[HTTP_HEADER] + if header = make_set_cookie_header(headers[HTTP_HEADER]) + headers[HTTP_HEADER] = header + end end mattr_accessor :always_write_cookie diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index e9b2fe3214..84c244c72a 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -3,6 +3,75 @@ require 'openssl' require 'active_support/key_generator' require 'active_support/message_verifier' +class CookieJarTest < ActiveSupport::TestCase + attr_reader :request + + def setup + @request = ActionDispatch::Request.new({}) + end + + def test_fetch + x = Object.new + assert_not request.cookie_jar.key?('zzzzzz') + assert_equal x, request.cookie_jar.fetch('zzzzzz', x) + assert_not request.cookie_jar.key?('zzzzzz') + end + + def test_fetch_exists + x = Object.new + request.cookie_jar['foo'] = 'bar' + assert_equal 'bar', request.cookie_jar.fetch('foo', x) + end + + def test_fetch_block + x = Object.new + assert_not request.cookie_jar.key?('zzzzzz') + assert_equal x, request.cookie_jar.fetch('zzzzzz') { x } + end + + def test_key_is_to_s + request.cookie_jar['foo'] = 'bar' + assert_equal 'bar', request.cookie_jar.fetch(:foo) + end + + def test_fetch_type_error + assert_raises(KeyError) do + request.cookie_jar.fetch(:omglolwut) + end + end + + def test_each + request.cookie_jar['foo'] = :bar + list = [] + request.cookie_jar.each do |k,v| + list << [k, v] + end + + assert_equal [['foo', :bar]], list + end + + def test_enumerable + request.cookie_jar['foo'] = :bar + actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } + assert_equal [['foo', 'bar']], actual + end + + def test_key_methods + assert !request.cookie_jar.key?(:foo) + assert !request.cookie_jar.has_key?("foo") + + request.cookie_jar[:foo] = :bar + assert request.cookie_jar.key?(:foo) + assert request.cookie_jar.has_key?("foo") + end + + def test_write_doesnt_set_a_nil_header + headers = {} + request.cookie_jar.write(headers) + assert !headers.include?('Set-Cookie') + end +end + class CookiesTest < ActionController::TestCase class CustomSerializer def self.load(value) @@ -14,16 +83,6 @@ class CookiesTest < ActionController::TestCase end end - class JSONWrapper - def initialize(obj) - @obj = obj - end - - def as_json(options = nil) - "wrapped: #{@obj.as_json(options)}" - end - end - class TestController < ActionController::Base def authenticate cookies["user_name"] = "david" @@ -88,11 +147,6 @@ class CookiesTest < ActionController::TestCase head :ok end - def set_wrapped_signed_cookie - cookies.signed[:user_id] = JSONWrapper.new(45) - head :ok - end - def get_signed_cookie cookies.signed[:user_id] head :ok @@ -103,6 +157,21 @@ class CookiesTest < ActionController::TestCase head :ok end + class JSONWrapper + def initialize(obj) + @obj = obj + end + + def as_json(options = nil) + "wrapped: #{@obj.as_json(options)}" + end + end + + def set_wrapped_signed_cookie + cookies.signed[:user_id] = JSONWrapper.new(45) + head :ok + end + def set_wrapped_encrypted_cookie cookies.encrypted[:foo] = JSONWrapper.new('bar') head :ok @@ -207,68 +276,18 @@ class CookiesTest < ActionController::TestCase tests TestController + SALT = 'b3c631c314c0bbca50c1b2843150fe33' + def setup super - @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2) - @request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.host = "www.nextangle.com" - end - - def test_fetch - x = Object.new - assert_not request.cookie_jar.key?('zzzzzz') - assert_equal x, request.cookie_jar.fetch('zzzzzz', x) - assert_not request.cookie_jar.key?('zzzzzz') - end - - def test_fetch_exists - x = Object.new - request.cookie_jar['foo'] = 'bar' - assert_equal 'bar', request.cookie_jar.fetch('foo', x) - end - - def test_fetch_block - x = Object.new - assert_not request.cookie_jar.key?('zzzzzz') - assert_equal x, request.cookie_jar.fetch('zzzzzz') { x } - end - - def test_key_is_to_s - request.cookie_jar['foo'] = 'bar' - assert_equal 'bar', request.cookie_jar.fetch(:foo) - end - - def test_fetch_type_error - assert_raises(KeyError) do - request.cookie_jar.fetch(:omglolwut) - end - end - - def test_each - request.cookie_jar['foo'] = :bar - list = [] - request.cookie_jar.each do |k,v| - list << [k, v] - end - assert_equal [['foo', :bar]], list - end + @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) - def test_enumerable - request.cookie_jar['foo'] = :bar - actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } - assert_equal [['foo', 'bar']], actual - end + @request.env["action_dispatch.signed_cookie_salt"] = + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT - def test_key_methods - assert !request.cookie_jar.key?(:foo) - assert !request.cookie_jar.has_key?("foo") - - request.cookie_jar[:foo] = :bar - assert request.cookie_jar.key?(:foo) - assert request.cookie_jar.has_key?("foo") + @request.host = "www.nextangle.com" end def test_setting_cookie @@ -1083,11 +1102,11 @@ class CookiesTest < ActionController::TestCase assert_equal "david", cookies[:user_name] get :noop - assert_nil @response.headers["Set-Cookie"] + assert !@response.headers.include?("Set-Cookie") assert_equal "david", cookies[:user_name] get :noop - assert_nil @response.headers["Set-Cookie"] + assert !@response.headers.include?("Set-Cookie") assert_equal "david", cookies[:user_name] end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 85cdcda655..82cc21b17a 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -196,7 +196,17 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type']) end - test "read content type without charset" do + test "read content type with default charset utf-8" do + original = ActionDispatch::Response.default_charset + begin + resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" }) + assert_equal('utf-8', resp.charset) + ensure + ActionDispatch::Response.default_charset = original + end + end + + test "read content type with charset utf-16" do jruby_skip "https://github.com/jruby/jruby/issues/3138" original = ActionDispatch::Response.default_charset @@ -283,6 +293,65 @@ class ResponseTest < ActiveSupport::TestCase end end +class ResponseHeadersTest < ActiveSupport::TestCase + def setup + @response = ActionDispatch::Response.create + @response.set_header 'Foo', '1' + end + + test 'have_header?' do + assert @response.have_header? 'Foo' + assert_not @response.have_header? 'foo' + assert_not @response.have_header? nil + end + + test 'get_header' do + assert_equal '1', @response.get_header('Foo') + assert_nil @response.get_header('foo') + assert_nil @response.get_header(nil) + end + + test 'set_header' do + assert_equal '2', @response.set_header('Foo', '2') + assert @response.have_header?('Foo') + assert_equal '2', @response.get_header('Foo') + + assert_nil @response.set_header('Foo', nil) + assert @response.have_header?('Foo') + assert_nil @response.get_header('Foo') + end + + test 'delete_header' do + assert_nil @response.delete_header(nil) + + assert_nil @response.delete_header('foo') + assert @response.have_header?('Foo') + + assert_equal '1', @response.delete_header('Foo') + assert_not @response.have_header?('Foo') + end + + test 'add_header' do + # Add a value to an existing header + assert_equal '1,2', @response.add_header('Foo', '2') + assert_equal '1,2', @response.get_header('Foo') + + # Add nil to an existing header + assert_equal '1,2', @response.add_header('Foo', nil) + assert_equal '1,2', @response.get_header('Foo') + + # Add nil to a nonexistent header + assert_nil @response.add_header('Bar', nil) + assert_not @response.have_header?('Bar') + assert_nil @response.get_header('Bar') + + # Add a value to a nonexistent header + assert_equal '1', @response.add_header('Bar', '1') + assert @response.have_header?('Bar') + assert_equal '1', @response.get_header('Bar') + end +end + class ResponseIntegrationTest < ActionDispatch::IntegrationTest test "response cache control from railsish app" do @app = lambda { |env| diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 6a3af32946..15d51e5d6c 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -497,17 +497,6 @@ module ActionDispatch private - def add_routes router, paths, anchor = true - paths.each do |path| - if String === path - path = Path::Pattern.from_string path - else - path - end - add_route @app, path, {}, [], {}, {} - end - end - def rails_env env, klass = ActionDispatch::Request klass.new(rack_env(env)) end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index af684e05c6..0e8127e29e 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -450,9 +450,9 @@ module ActionView disable_with_text ||= value.clone tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) else - tag_options.delete("data-disable-with") tag_options["data"].delete(:disable_with) if tag_options["data"] end + tag_options.delete("data-disable-with") end tag :input, tag_options diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index cabba967c9..a74a5e05f3 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -401,7 +401,7 @@ module ActionView # ==== Parameters # * <tt>formats</tt> - The formats accepted to this layout # * <tt>require_layout</tt> - If set to true and layout is not found, - # an ArgumentError exception is raised (defaults to false) + # an +ArgumentError+ exception is raised (defaults to false) # # ==== Returns # * <tt>template</tt> - The template object for the default layout (or nil) diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index a9d9562580..de1eb89dc5 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -64,6 +64,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_check_box_tag_disabled + actual = check_box_tag "admin","1", false, disabled: true + expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />) + assert_dom_equal expected, actual + end + + def test_check_box_tag_default_checked + actual = check_box_tag "admin","1", true + expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />) + assert_dom_equal expected, actual + end + def test_check_box_tag_id_sanitized label_elem = root_elem(check_box_tag("project[2][admin]")) assert_match VALID_HTML_ID, label_elem['id'] @@ -351,12 +363,18 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end - def test_text_field_disabled + def test_text_field_tag_disabled actual = text_field_tag "title", "Hello!", disabled: true expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />) assert_dom_equal expected, actual end + def test_text_field_tag_with_placeholder_option + actual = text_field_tag "title", "Hello!", placeholder: 'Enter search term...' + expected = %(<input id="title" name="title" placeholder="Enter search term..." type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + def test_text_field_tag_with_multiple_options actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80 expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />) @@ -450,21 +468,21 @@ class FormTagHelperTest < ActionView::TestCase ActionView::Base.automatically_disable_submit_tag = true end - def test_data_disable_with_string + def test_submit_tag_having_data_disable_with_string assert_dom_equal( %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) ) end - def test_data_disable_with_boolean + def test_submit_tag_having_data_disable_with_boolean assert_dom_equal( %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" }) ) end - def test_data_hash_disable_with_boolean + def test_submit_tag_having_data_hash_disable_with_boolean assert_dom_equal( %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } }) @@ -485,6 +503,14 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_submit_tag_doesnt_have_data_disable_with_twice + assert_equal( + %(<input type="submit" name="commit" value="Save" data-confirm="Are you sure?" data-disable-with="Processing..." />), + submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) + ) + end + + def test_button_tag assert_dom_equal( %(<button name="button" type="submit">Button</button>), diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index bf0120122d..a3368cd197 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -119,10 +119,10 @@ The preferred method to halt a callback chain from now on is to explicitly `throw(:abort)`. - In the past, returning `false` in an ActiveModel or ActiveModel::Validations - `before_` callback had the side effect of halting the callback chain. + In the past, returning `false` in an Active Model `before_` callback had + the side effect of halting the callback chain. This is not recommended anymore and, depending on the value of the - `config.active_support.halt_callback_chains_on_return_false` option, will + `ActiveSupport.halt_callback_chains_on_return_false` option, will either not work at all or display a deprecation warning. diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index b4fa378601..d2c6a89cc2 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -17,8 +17,9 @@ module ActiveModel module ForbiddenAttributesProtection # :nodoc: protected def sanitize_for_mass_assignment(attributes) - if attributes.respond_to?(:permitted?) && !attributes.permitted? - raise ActiveModel::ForbiddenAttributesError + if attributes.respond_to?(:permitted?) + raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? + attributes.to_h else attributes end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6c0722e43c..6a40d32ef9 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,16 @@ +* Allow fixtures files to set the model class in the YAML file itself. + + To load the fixtures file `accounts.yml` as the `User` model, use: + + _fixture: + model_class: User + david: + name: David + + Fixes #9516. + + *Roque Pinel* + * Don't require a database connection to load a class which uses acceptance validations. @@ -1104,7 +1117,7 @@ In the past, returning `false` in an Active Record `before_` callback had the side effect of halting the callback chain. This is not recommended anymore and, depending on the value of the - `config.active_support.halt_callback_chains_on_return_false` option, will + `ActiveSupport.halt_callback_chains_on_return_false` option, will either not work at all or display a deprecation warning. *claudiob* diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 74b44810d4..3c4c8f10ec 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -5,8 +5,8 @@ module ActiveRecord FromDatabase.new(name, value, type) end - def from_user(name, value, type) - FromUser.new(name, value, type) + def from_user(name, value, type, original_attribute = nil) + FromUser.new(name, value, type, original_attribute) end def with_cast_value(name, value, type) @@ -26,10 +26,11 @@ module ActiveRecord # This method should not be called directly. # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type) + def initialize(name, value_before_type_cast, type, original_attribute = nil) @name = name @value_before_type_cast = value_before_type_cast @type = type + @original_attribute = original_attribute end def value @@ -38,21 +39,33 @@ module ActiveRecord @value end + def original_value + if assigned? + original_attribute.original_value + else + type_cast(value_before_type_cast) + end + end + def value_for_database type.serialize(value) end - def changed_from?(old_value) - type.changed?(old_value, value, value_before_type_cast) + def changed? + changed_from_assignment? || changed_in_place? + end + + def changed_in_place? + has_been_read? && type.changed_in_place?(original_value_for_database, value) end - def changed_in_place_from?(old_value) - has_been_read? && type.changed_in_place?(old_value, value) + def forgetting_assignment + with_value_from_database(value_for_database) end def with_value_from_user(value) type.assert_valid_value(value) - self.class.from_user(name, value, type) + self.class.from_user(name, value, type, self) end def with_value_from_database(value) @@ -64,7 +77,7 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type) + self.class.new(name, value_before_type_cast, type, original_attribute) end def type_cast(*) @@ -97,16 +110,39 @@ module ActiveRecord protected + attr_reader :original_attribute + alias_method :assigned?, :original_attribute + def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup end end + def changed_from_assignment? + assigned? && type.changed?(original_value, value, value_before_type_cast) + end + + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end + end + + def _original_value_for_database + value_for_database + end + class FromDatabase < Attribute # :nodoc: def type_cast(value) type.deserialize(value) end + + def _original_value_for_database + value_before_type_cast + end end class FromUser < Attribute # :nodoc: diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index 501590cf0e..fb8ad9163e 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -4,8 +4,7 @@ module ActiveRecord class Attribute # :nodoc: class UserProvidedDefault < FromUser def initialize(name, value, type, database_default) - super(name, value, type) - @database_default = database_default + super(name, value, type, database_default) end def type_cast(value) @@ -16,17 +15,9 @@ module ActiveRecord end end - def changed_from?(old_value) - super || changed_from?(database_default.value) - end - def with_type(type) - self.class.new(name, value_before_type_cast, type, database_default) + self.class.new(name, value_before_type_cast, type, original_attribute) end - - protected - - attr_reader :database_default end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 43c15841c2..e8a782ed13 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -35,23 +35,22 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - clear_changes_information + @mutation_tracker = nil + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new end end - def init_internals - super - @mutation_tracker = AttributeMutationTracker.new(@attributes, @attributes.dup) - end - def initialize_dup(other) # :nodoc: super - @mutation_tracker = AttributeMutationTracker.new(@attributes, - self.class._default_attributes.deep_dup) + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + @mutation_tracker = nil end def changes_applied - @previous_mutation_tracker = @mutation_tracker + @previous_mutation_tracker = mutation_tracker @changed_attributes = HashWithIndifferentAccess.new store_original_attributes end @@ -81,7 +80,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(@mutation_tracker.changed_values).freeze + super.reverse_merge(mutation_tracker.changed_values).freeze end end @@ -96,17 +95,24 @@ module ActiveRecord end def attribute_changed_in_place?(attr_name) - @mutation_tracker.changed_in_place?(attr_name) + mutation_tracker.changed_in_place?(attr_name) end private + def mutation_tracker + unless defined?(@mutation_tracker) + @mutation_tracker = nil + end + @mutation_tracker ||= AttributeMutationTracker.new(@attributes) + end + def changes_include?(attr_name) - super || @mutation_tracker.changed?(attr_name) + super || mutation_tracker.changed?(attr_name) end def clear_attribute_change(attr_name) - @mutation_tracker.forget_change(attr_name) + mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -122,7 +128,8 @@ module ActiveRecord end def store_original_attributes - @mutation_tracker = @mutation_tracker.now_tracking(@attributes) + @attributes = @attributes.map(&:forgetting_assignment) + @mutation_tracker = nil end def previous_mutation_tracker diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index 08eb8ba7ac..ba7348918b 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -1,14 +1,13 @@ module ActiveRecord class AttributeMutationTracker # :nodoc: - def initialize(attributes, original_attributes) + def initialize(attributes) @attributes = attributes - @original_attributes = original_attributes end def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) - result[attr_name] = original_attributes.fetch_value(attr_name) + result[attr_name] = attributes[attr_name].original_value end end end @@ -16,49 +15,34 @@ module ActiveRecord def changes attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) - result[attr_name] = [original_attributes.fetch_value(attr_name), attributes.fetch_value(attr_name)] + result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] end end end def changed?(attr_name) attr_name = attr_name.to_s - modified?(attr_name) || changed_in_place?(attr_name) + attributes[attr_name].changed? end def changed_in_place?(attr_name) - original_database_value = original_attributes[attr_name].value_before_type_cast - attributes[attr_name].changed_in_place_from?(original_database_value) + attributes[attr_name].changed_in_place? end def forget_change(attr_name) attr_name = attr_name.to_s - original_attributes[attr_name] = attributes[attr_name].dup - end - - def now_tracking(new_attributes) - AttributeMutationTracker.new(new_attributes, clean_copy_of(new_attributes)) + attributes[attr_name] = attributes[attr_name].forgetting_assignment end protected - attr_reader :attributes, :original_attributes + attr_reader :attributes private def attr_names attributes.keys end - - def modified?(attr_name) - attributes[attr_name].changed_from?(original_attributes.fetch_value(attr_name)) - end - - def clean_copy_of(attributes) - attributes.map do |attr| - attr.with_value_from_database(attr.value_for_database) - end - end end class NullMutationTracker # :nodoc: diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d6b661ff76..2fc5e410f9 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -42,7 +42,7 @@ module ActiveRecord # # ActiveRecord::Base.establish_connection(:production) # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # The exceptions +AdapterNotSpecified+, +AdapterNotFound+ and +ArgumentError+ # may be returned on an error. def establish_connection(spec = nil) spec ||= DEFAULT_ENV.call.to_sym diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 8132310c91..f969556c50 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -17,24 +17,39 @@ module ActiveRecord def initialize(file) @file = file - @rows = nil end def each(&block) rows.each(&block) end + def model_class + config_row['model_class'] + end private def rows - return @rows if @rows + @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' } + end + + def config_row + @config_row ||= begin + row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' } + if row + row.last + else + {'model_class': nil} + end + end + end - begin + def raw_rows + @raw_rows ||= begin data = YAML.load(render(IO.read(@file))) + data ? validate(data).to_a : [] rescue ArgumentError, Psych::SyntaxError => error raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace end - @rows = data ? validate(data).to_a : [] end def render(content) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index f1dc56df63..59df3c78f3 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -395,6 +395,20 @@ module ActiveRecord # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. + # + # == Configure the fixture model class + # + # It's possible to set the fixture's model class directly in the YAML file. + # This is helpful when fixtures are loaded outside tests and + # +set_fixture_class+ is not available (e.g. + # when running <tt>rake db:fixtures:load</tt>). + # + # _fixture: + # model_class: User + # david: + # name: David + # + # Any fixtures labeled "_fixture" are safely ignored. class FixtureSet #-- # An instance of FixtureSet is normally stored in a single YAML file and @@ -578,21 +592,16 @@ module ActiveRecord @name = name @path = path @config = config - @model_class = nil - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? - @model_class = class_name - else - @model_class = class_name.safe_constantize if class_name - end + self.model_class = class_name + + @fixtures = read_fixture_files(path) @connection = connection @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : self.class.default_fixture_table_name(name, config) ) - - @fixtures = read_fixture_files path, @model_class end def [](x) @@ -761,13 +770,25 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect(&:name) end - def read_fixture_files(path, model_class) + def model_class=(class_name) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? + @model_class = class_name + else + @model_class = class_name.safe_constantize if class_name + end + end + + # Loads the fixtures from the YAML file at +path+. + # If the file sets the +model_class+ and current instance value is not set, + # it uses the file value. + def read_fixture_files(path) yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path(path)] yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| + self.model_class ||= fh.model_class if fh.model_class fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7b53f6e5a0..3f02f73a5a 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -211,7 +211,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutation_tracker", @mutation_tracker) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index eb53a18f0f..ccb0ab18ae 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -558,11 +558,8 @@ module ActiveRecord end def where!(opts, *rest) # :nodoc: - if Hash === opts - opts = sanitize_forbidden_attributes(opts) - references!(PredicateBuilder.references(opts)) - end - + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) if Hash === opts self.where_clause += where_clause_factory.build(opts, rest) self end @@ -619,6 +616,7 @@ module ActiveRecord end def having!(opts, *rest) # :nodoc: + opts = sanitize_forbidden_attributes(opts) references!(PredicateBuilder.references(opts)) if Hash === opts self.having_clause += having_clause_factory.build(opts, rest) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index b67201d94d..52d197718e 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -177,7 +177,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_read_attributes_before_type_cast_on_boolean - bool = Boolean.create({ "value" => false }) + bool = Boolean.create!({ "value" => false }) if RUBY_PLATFORM =~ /java/ # JRuby will return the value before typecast as string assert_equal "0", bool.reload.attributes_before_type_cast["value"] @@ -542,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase developer.save! - assert_equal "50000", developer.salary_before_type_cast - assert_equal 1337, developer.name_before_type_cast - assert_equal 50000, developer.salary assert_equal "1337", developer.name end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 0f216b7a13..a24a4fc6a4 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -182,12 +182,49 @@ module ActiveRecord assert attribute.has_been_read? end + test "an attribute is not changed if it hasn't been assigned or mutated" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + + refute attribute.changed? + end + + test "an attribute is changed if it's been assigned a new value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + changed = attribute.with_value_from_user(2) + + assert changed.changed? + end + + test "an attribute is not changed if it's assigned the same value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + unchanged = attribute.with_value_from_user(1) + + refute unchanged.changed? + end + test "an attribute can not be mutated if it has not been read, and skips expensive calculations" do type_which_raises_from_all_methods = Object.new attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) - assert_not attribute.changed_in_place_from?("bar") + assert_not attribute.changed_in_place? + end + + test "an attribute is changed if it has been mutated" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + attribute.value << "!" + + assert attribute.changed_in_place? + assert attribute.changed? + end + + test "an attribute can forget its changes" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + changed = attribute.with_value_from_user("foo") + forgotten = changed.forgetting_assignment + + assert changed.changed? # sanity check + refute forgotten.changed? end test "with_value_from_user validates the value" do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index aa10817527..d904b802fa 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -681,4 +681,36 @@ class CalculationsTest < ActiveRecord::TestCase end assert block_called end + + def test_having_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + params = protected_params.new(credit_limit: '50') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Account.group(:id).having(params) + end + + result = Account.group(:id).having(params.permit!) + assert_equal 50, result[0].credit_limit + assert_equal 50, result[1].credit_limit + assert_equal 50, result[2].credit_limit + end end diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index 92efa8aca7..242e7a9bec 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -123,6 +123,18 @@ END end end + def test_removes_fixture_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal(['second_welcome'], fh.each.map { |name, _| name }) + end + end + + def test_extracts_model_class_from_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal 'Post', fh.model_class + end + end + private def tmp_yaml(name, contents) t = Tempfile.new name diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index e3c4bd9f4a..a0eaa66e94 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,15 +7,16 @@ require 'models/binary' require 'models/book' require 'models/bulb' require 'models/category' +require 'models/comment' require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' +require 'models/doubloon' require 'models/joke' require 'models/matey' require 'models/parrot' require 'models/pirate' -require 'models/doubloon' require 'models/post' require 'models/randomly_named_c1' require 'models/reply' @@ -516,6 +517,38 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase end end +class FixtureWithSetModelClassTest < ActiveRecord::TestCase + fixtures :other_posts, :other_comments + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_fixture_class_defined_in_yaml + assert_kind_of Post, other_posts(:second_welcome) + end + + def test_loads_the_associations_to_fixtures_with_set_model_class + post = other_posts(:second_welcome) + comment = other_comments(:second_greetings) + assert_equal [comment], post.comments + assert_equal post, comment.post + end +end + +class SetFixtureClassPrevailsTest < ActiveRecord::TestCase + set_fixture_class bad_posts: Post + fixtures :bad_posts + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_set_fixture_class + assert_kind_of Post, bad_posts(:bad_welcome) + end +end + class CheckSetTableNameFixturesTest < ActiveRecord::TestCase set_fixture_class :funny_jokes => Joke fixtures :funny_jokes diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 80d17b8114..9169207b0a 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase owner.update_column :updated_at, Time.current key = owner.cache_key - assert pet.touch + travel(1.second) do + assert pet.touch + end assert_not_equal key, owner.reload.cache_key end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 6af31017d6..c3a1471205 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -276,5 +276,31 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + + def test_where_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + author = authors(:david) + params = protected_params.new(name: author.name) + assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) } + assert_equal author, Author.where(params.permit!).first + end end end diff --git a/activerecord/test/fixtures/bad_posts.yml b/activerecord/test/fixtures/bad_posts.yml new file mode 100644 index 0000000000..addee8e3bf --- /dev/null +++ b/activerecord/test/fixtures/bad_posts.yml @@ -0,0 +1,9 @@ +# Please do not use this fixture without `set_fixture_class` as Post + +_fixture: + model_class: BadPostModel + +bad_welcome: + author_id: 1 + title: Welcome to the another weblog + body: It's really nice today diff --git a/activerecord/test/fixtures/other_comments.yml b/activerecord/test/fixtures/other_comments.yml new file mode 100644 index 0000000000..55e8216ec7 --- /dev/null +++ b/activerecord/test/fixtures/other_comments.yml @@ -0,0 +1,6 @@ +_fixture: + model_class: Comment + +second_greetings: + post: second_welcome + body: Thank you for the second welcome diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml new file mode 100644 index 0000000000..39ff763547 --- /dev/null +++ b/activerecord/test/fixtures/other_posts.yml @@ -0,0 +1,7 @@ +_fixture: + model_class: Post + +second_welcome: + author_id: 1 + title: Welcome to the another weblog + body: It's really nice today diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8c1f14bd36..0d90cbb110 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -144,9 +144,6 @@ class Author < ActiveRecord::Base has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" - scope :relation_include_posts, -> { includes(:posts) } - scope :relation_include_tags, -> { includes(:tags) } - attr_accessor :post_log after_initialize :set_post_log diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index b26035d944..ddc9dcaf29 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -21,9 +21,3 @@ end class DeadParrot < Parrot belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id end - -class FunkyParrot < Parrot - before_destroy do - raise "before_destroy was called" - end -end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index ad12f00d42..a4a9c6b0d4 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -37,7 +37,6 @@ class Person < ActiveRecord::Base has_many :essays, primary_key: "first_name", foreign_key: "writer_id" scope :males, -> { where(:gender => 'M') } - scope :females, -> { where(:gender => 'F') } end class PersonWithDependentDestroyJobs < ActiveRecord::Base diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index a39344e464..19588d622c 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -291,18 +291,15 @@ In the past, callbacks could only be halted by explicitly providing a terminator and by having a callback match the conditions of the terminator. -* Add `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` +* Add `ActiveSupport.halt_callback_chains_on_return_false` - Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` + Setting `ActiveSupport.halt_callback_chains_on_return_false` to `true` will let an app support the deprecated way of halting Active Record, - Active Model and Active Model validations callback chains by returning `false`. + and Active Model callback chains by returning `false`. Setting the value to `false` will tell the app to ignore any `false` value returned by those callbacks, and only halt the chain upon `throw(:abort)`. - The value can also be set with the Rails configuration option - `config.active_support.halt_callback_chains_on_return_false`. - When the configuration option is missing, its value is `true`, so older apps ported to Rails 5.0 will not break (but display a deprecation warning). For new Rails 5.0 apps, its value is set to `false` in an initializer, so diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index d236dbd200..93878518d7 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' - s.add_dependency 'concurrent-ruby', '~> 1.0.0.pre2', '< 2.0.0' + s.add_dependency 'concurrent-ruby', '~> 1.0.0.pre3', '< 2.0.0' s.add_dependency 'method_source' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 588d6c49f9..63277a65b4 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -75,11 +75,11 @@ module ActiveSupport cattr_accessor :test_order # :nodoc: def self.halt_callback_chains_on_return_false - Callbacks::CallbackChain.halt_and_display_warning_on_return_false + Callbacks.halt_and_display_warning_on_return_false end def self.halt_callback_chains_on_return_false=(value) - Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value + Callbacks.halt_and_display_warning_on_return_false = value end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 3db9ea2ac0..252374e817 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/filters' require 'active_support/deprecation' require 'thread' @@ -66,6 +67,12 @@ module ActiveSupport CALLBACK_FILTER_TYPES = [:before, :after, :around] + # If true, Active Record and Active Model callbacks returning +false+ will + # halt the entire callback chain and display a deprecation message. + # If false, callback chains will only be halted by calling +throw :abort+. + # Defaults to +true+. + mattr_accessor(:halt_and_display_warning_on_return_false) { true } + # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -450,12 +457,6 @@ module ActiveSupport attr_reader :name, :config - # If true, any callback returning +false+ will halt the entire callback - # chain and display a deprecation message. If false, callback chains will - # only be halted by calling +throw :abort+. Defaults to +true+. - class_attribute :halt_and_display_warning_on_return_false - self.halt_and_display_warning_on_return_false = true - def initialize(name, config) @name = name @config = { @@ -760,7 +761,7 @@ module ActiveSupport terminate = true catch(:abort) do result = result_lambda.call if result_lambda.is_a?(Proc) - if CallbackChain.halt_and_display_warning_on_return_false && result == false + if Callbacks.halt_and_display_warning_on_return_false && result == false display_deprecation_warning_for_false_terminator else terminate = false diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index c60e833441..d589b67bf7 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -26,7 +26,7 @@ class Date Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start) end - # Returns week start day symbol (e.g. :monday), or raises an ArgumentError for invalid day symbol. + # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol. def find_beginning_of_week!(week_start) raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start) week_start diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 05679f1dd0..07a282e8b6 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -64,7 +64,7 @@ class Hash alias_method :to_options!, :symbolize_keys! # Validates all keys in a hash match <tt>*valid_keys</tt>, raising - # ArgumentError on a mismatch. + # +ArgumentError+ on a mismatch. # # Note that keys are treated differently than HashWithIndifferentAccess, # meaning that string and symbol keys will not match. diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index a084177b9f..bf175a8a70 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -53,7 +53,7 @@ class Module def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -119,7 +119,7 @@ class Module def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 133d3938eb..877dc84ec8 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -53,7 +53,7 @@ class Time # Returns a TimeZone instance matching the time zone provided. # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. - # Raises an ArgumentError for invalid time zones. + # Raises an +ArgumentError+ for invalid time zones. # # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...> diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb index 4c6a0801b8..affb84cda5 100644 --- a/activesupport/lib/active_support/testing/file_fixtures.rb +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -18,7 +18,7 @@ module ActiveSupport # Returns a +Pathname+ to the fixture file named +fixture_name+. # - # Raises ArgumentError if +fixture_name+ can't be found. + # Raises +ArgumentError+ if +fixture_name+ can't be found. def file_fixture(fixture_name) path = Pathname.new(File.join(file_fixture_path, fixture_name)) diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 1de0a19998..edf8b30a0a 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -41,7 +41,23 @@ module ActiveSupport pid = fork do read.close yield - write.puts [Marshal.dump(self.dup)].pack("m") + begin + if error? + failures.map! { |e| + begin + Marshal.dump e + e + rescue TypeError + ex = Exception.new e.message + ex.set_backtrace e.backtrace + Minitest::UnexpectedError.new ex + end + } + end + result = Marshal.dump(self.dup) + end + + write.puts [result].pack("m") exit! end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index a82a7dfea0..3592dcba39 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -41,6 +41,9 @@ module ActiveSupport 'Time' end + PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } + PRECISIONS[0] = '%FT%T'.freeze + include Comparable attr_reader :time_zone @@ -142,11 +145,7 @@ module ActiveSupport # # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - fraction = if fraction_digits.to_i > 0 - (".%06i" % time.usec)[0, fraction_digits.to_i + 1] - end - - "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" + "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" end alias_method :iso8601, :xmlschema diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 4f47e0519f..3b00ff87a0 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -776,7 +776,7 @@ module CallbacksTest class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase def setup - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true + ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true end def test_returning_false_does_not_halt_callback_if_config_variable_is_true @@ -789,7 +789,7 @@ module CallbacksTest class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase def setup - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = false + ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false end def test_returning_false_does_not_halt_callback_if_config_variable_is_false diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 128c5e3d1a..0b0f3a2808 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -69,6 +69,20 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase end end assert_equal "invalid attribute name: 1nvalid", exception.message + + exception = assert_raises NameError do + Class.new do + mattr_reader "valid_part\ninvalid_part" + end + end + assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message + + exception = assert_raises NameError do + Class.new do + mattr_writer "valid_part\ninvalid_part" + end + end + assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message end def test_should_use_default_value_if_block_passed diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index ccb7f02331..c40f0bacbf 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -110,14 +110,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase @twz += 0.1234560001 # advance the time by a fraction of a second assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3) assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6) - assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12) + assert_equal "1999-12-31T19:00:00.123456000100-05:00", @twz.xmlschema(12) end def test_xmlschema_with_fractional_seconds_lower_than_hundred_thousand @twz += 0.001234 # advance the time by a fraction assert_equal "1999-12-31T19:00:00.001-05:00", @twz.xmlschema(3) assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(6) - assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12) + assert_equal "1999-12-31T19:00:00.001234000000-05:00", @twz.xmlschema(12) end def test_xmlschema_with_nil_fractional_seconds diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 60790b33a4..999c533fb3 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -932,8 +932,11 @@ If you set the `:dependent` option to: * `:destroy`, when the object is destroyed, `destroy` will be called on its associated objects. -* `:delete`, when the object is destroyed, all its associated objects will be +* `:delete_all`, when the object is destroyed, all its associated objects will be deleted directly from the database without calling their `destroy` method. +* `:nullify`, causes the foreign key to be set to `NULL`. Callbacks are not executed. +* `:restrict_with_exception`, causes an exception to be raised if there is an associated record +* `:restrict_with_error`, causes an error to be added to the owner if there is an associated object WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index f0d87e4dc8..afdb0ba7eb 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -535,7 +535,7 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. -* `config.active_support.halt_callback_chains_on_return_false` specifies whether ActiveRecord, ActiveModel and ActiveModel::Validations callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`. +* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`. * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 52464a1c51..490bda3571 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -55,23 +55,26 @@ Upgrading from Rails 4.2 to Rails 5.0 ### Halting callback chains by returning `false` -In Rails 4.2, when a 'before' callback returns `false` in ActiveRecord, -ActiveModel and ActiveModel::Validations, then the entire callback chain -is halted. In other words, successive 'before' callbacks are not executed, -and neither is the action wrapped in callbacks. +In Rails 4.2, when a 'before' callback returns `false` in Active Record +and Active Model, then the entire callback chain is halted. In other words, +successive 'before' callbacks are not executed, and neither is the action wrapped +in callbacks. -In Rails 5.0, returning `false` in a callback will not have this side effect -of halting the callback chain. Instead, callback chains must be explicitly -halted by calling `throw(:abort)`. +In Rails 5.0, returning `false` in an Active Record or Active Model callback +will not have this side effect of halting the callback chain. Instead, callback +chains must be explicitly halted by calling `throw(:abort)`. -When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in a callback -will still halt the callback chain, but you will receive a deprecation warning -about this upcoming change. +When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in those kind of +callbacks will still halt the callback chain, but you will receive a deprecation +warning about this upcoming change. When you are ready, you can opt into the new behavior and remove the deprecation warning by adding the following configuration to your `config/application.rb`: - config.active_support.halt_callback_chains_on_return_false = false + ActiveSupport.halt_callback_chains_on_return_false = false + +Note that this option will not affect Active Support callbacks since they never +halted the chain when any value was returned. See [#17227](https://github.com/rails/rails/pull/17227) for more details. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index f007d11c72..3e45a09dec 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,65 @@ +* Add fail fast to `bin/rails test` + + Adding `--fail-fast` or `-f` when running tests will interrupt the run on + the first failure: + + ``` + # Running: + + ................................................S......E + + ArgumentError: Wups! Bet you didn't expect this! + test/models/bunny_test.rb:19:in `block in <class:BunnyTest>' + + bin/rails test test/models/bunny_test.rb:18 + + ....................................F + + This failed + + bin/rails test test/models/bunny_test.rb:14 + + Interrupted. Exiting... + + + Finished in 0.051427s, 1808.3872 runs/s, 1769.4972 assertions/s. + + ``` + + Note that any unexpected errors don't abort the run. + + *Kasper Timm Hansen* + +* Add inline output to `bin/rails test` + + Any failures or errors (and skips if running in verbose mode) are output + during a test run: + + ``` + # Running: + + .....S..........................................F + + This failed + + bin/rails test test/models/bunny_test.rb:14 + + .................................E + + ArgumentError: Wups! Bet you didn't expect this! + test/models/bunny_test.rb:19:in `block in <class:BunnyTest>' + + bin/rails test test/models/bunny_test.rb:18 + + .................... + + Finished in 0.069708s, 1477.6019 runs/s, 1448.9106 assertions/s. + ``` + + Output can be deferred to after a run with the `--defer-output` option. + + *Kasper Timm Hansen* + * Fix displaying mailer previews on non local requests when config `action_mailer.show_previews` is set @@ -234,10 +296,11 @@ Newly generated Rails apps have a new initializer called `callback_terminator.rb` which sets the value of the configuration option - `config.active_support.halt_callback_chains_on_return_false` to `false`. + `ActiveSupport.halt_callback_chains_on_return_false` to `false`. - As a result, new Rails apps do not halt callback chains when a callback - returns `false`; only when they are explicitly halted with `throw(:abort)`. + As a result, new Rails apps do not halt Active Record and Active Model + callback chains when a callback returns `false`; only when they are + explicitly halted with `throw(:abort)`. The terminator is *not* added when running `rake rails:update`, so returning `false` will still work on old apps ported to Rails 5, displaying a diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8075068b3f..175965e565 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -156,15 +156,6 @@ module Rails self end - # Implements call according to the Rack API. It simply - # dispatches the request to the underlying middleware stack. - def call(env) - req = ActionDispatch::Request.new env - env["ORIGINAL_FULLPATH"] = req.fullpath - env["ORIGINAL_SCRIPT_NAME"] = req.script_name - super(env) - end - # Reload application routes regardless if they changed or not. def reload_routes! routes_reloader.reload! @@ -514,5 +505,18 @@ module Rails end end end + + private + + def build_request(env) + req = super + env["ORIGINAL_FULLPATH"] = req.fullpath + env["ORIGINAL_SCRIPT_NAME"] = req.script_name + req + end + + def build_middleware + config.app_middleware + super + end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4fc7a1db62..75112f29b6 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -36,7 +36,6 @@ module Rails @time_zone = "UTC" @beginning_of_week = :monday @log_level = nil - @middleware = app_middleware @generators = app_generators @cache_store = [ :file_store, "#{root}/tmp/cache/" ] @railties_order = [:all] diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index d99d27a756..30eafd59f2 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -33,9 +33,9 @@ module Rails # config.middleware.delete ActionDispatch::Flash # class MiddlewareStackProxy - def initialize - @operations = [] - @delete_operations = [] + def initialize(operations = [], delete_operations = []) + @operations = operations + @delete_operations = delete_operations end def insert_before(*args, &block) @@ -71,6 +71,19 @@ module Rails other end + + def +(other) # :nodoc: + MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations) + end + + protected + def operations + @operations + end + + def delete_operations + @delete_operations + end end class Generators #:nodoc: diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index e90d41cbec..c4bb7d2109 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -2,6 +2,7 @@ require 'rails/railtie' require 'rails/engine/railties' require 'active_support/core_ext/module/delegation' require 'pathname' +require 'thread' module Rails # <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of @@ -434,6 +435,7 @@ module Rails @env_config = nil @helpers = nil @routes = nil + @app_build_lock = Mutex.new super end @@ -504,10 +506,13 @@ module Rails # Returns the underlying rack application for this engine. def app - @app ||= begin - config.middleware = config.middleware.merge_into(default_middleware_stack) - config.middleware.build(endpoint) - end + @app || @app_build_lock.synchronize { + @app ||= begin + stack = default_middleware_stack + config.middleware = build_middleware.merge_into(stack) + config.middleware.build(endpoint) + end + } end # Returns the endpoint for this engine. If none is registered, @@ -518,11 +523,8 @@ module Rails # Define the Rack API for this engine. def call(env) - env.merge!(env_config) - req = ActionDispatch::Request.new env - req.routes = routes - req.engine_script_name = req.script_name - app.call(env) + req = build_request env + app.call req.env end # Defines additional Rack env configuration that is added on each call. @@ -689,5 +691,19 @@ module Rails def _all_load_paths #:nodoc: @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq end + + private + + def build_request(env) + env.merge!(env_config) + req = ActionDispatch::Request.new env + req.routes = routes + req.engine_script_name = req.script_name + req + end + + def build_middleware + config.middleware + end end end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 62a4139d07..b4ddee3b1b 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -4,17 +4,14 @@ module Rails class Engine class Configuration < ::Rails::Railtie::Configuration attr_reader :root - attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths + attr_accessor :middleware + attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths def initialize(root=nil) super() @root = root @generators = app_generators.dup - end - - # Returns the middleware stack for the engine. - def middleware - @middleware ||= Rails::Configuration::MiddlewareStackProxy.new + @middleware = Rails::Configuration::MiddlewareStackProxy.new end # Holds generators configuration: diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt new file mode 100644 index 0000000000..f80631bac6 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt @@ -0,0 +1,8 @@ + +<% unless options.api? -%> +//= link_tree ../images +<% end -%> +<% unless options.skip_javascript -%> +//= link_directory ../javascripts .js +<% end -%> +//= link_directory ../stylesheets .css diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt deleted file mode 100644 index 3325553f57..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt +++ /dev/null @@ -1,8 +0,0 @@ - -<% unless options.api? -%> -//= link_tree ./images -<% end -%> -<% unless options.skip_javascript -%> -//= link ./javascripts/application.js -<% end -%> -//= link ./stylesheets/application.css diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 8fea30189e..910c4e743e 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -106,7 +106,7 @@ task default: :test def test_dummy_assets template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true - template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/manifest.js", force: true + template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true end def test_dummy_clean @@ -124,7 +124,7 @@ task default: :test end def assets_manifest - template "rails/engine_manifest.js", "app/assets/#{underscored_name}_manifest.js" + template "rails/engine_manifest.js", "app/assets/config/#{underscored_name}_manifest.js" end def stylesheets diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js index ace695a811..8d21b2b6fb 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js @@ -1,11 +1,11 @@ <% unless api? -%> -//= link_tree ./images +//= link_tree ../images <% end -%> <% unless options.skip_javascript -%> -//= link ./javascripts/application.js +//= link_directory ../javascripts .js <% end -%> -//= link ./stylesheets/application.css +//= link_directory ../stylesheets .css <% if mountable? && !api? -%> //= link <%= underscored_name %>_manifest.js <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js index f8ef26982a..2f23844f5e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js @@ -1,6 +1,6 @@ <% if mountable? -%> -<% unless options.skip_javascript -%> -//= link ./javascripts/<%= namespaced_name %>/application.js +<% if !options.skip_javascript -%> +//= link_directory ../javascripts/<%= namespaced_name %> .js <% end -%> -//= link ./stylesheets/<%= namespaced_name %>/application.css +//= link_directory ../stylesheets/<%= namespaced_name %> .css <% end -%> diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 1923433c4a..3a0a58df88 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -14,6 +14,8 @@ module Minitest opts.separator "" opts.separator " bin/rails test test/controllers test/integration/login_test.rb" opts.separator "" + opts.separator "By default test failures and errors are reported inline during a run." + opts.separator "" opts.separator "Rails options:" opts.on("-e", "--environment ENV", @@ -26,6 +28,16 @@ module Minitest options[:full_backtrace] = true end + opts.on("-d", "--defer-output", + "Output test failures and errors after the test run") do + options[:output_inline] = false + end + + opts.on("-f", "--fail-fast", + "Abort test run on first failure") do + options[:fail_fast] = true + end + options[:patterns] = opts.order! end diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 09b8675cf8..8f1116b6af 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -6,8 +6,25 @@ module Rails class_attribute :executable self.executable = "bin/rails test" + def record(result) + super + + if output_inline? && result.failure && (!result.skipped? || options[:verbose]) + io.puts + io.puts + io.puts result.failures.map(&:message) + io.puts + io.puts format_rerun_snippet(result) + io.puts + end + + if fail_fast? && result.failure && !result.error? && !result.skipped? + raise Interrupt + end + end + def report - return if filtered_results.empty? + return if output_inline? || filtered_results.empty? io.puts io.puts "Failed tests:" io.puts @@ -15,10 +32,7 @@ module Rails end def aggregated_results # :nodoc: - filtered_results.map do |result| - location, line = result.method(result.name).source_location - "#{self.executable} #{relative_path_for(location)}:#{line}" - end.join "\n" + filtered_results.map { |result| format_rerun_snippet(result) }.join "\n" end def filtered_results @@ -32,5 +46,19 @@ module Rails def relative_path_for(file) file.sub(/^#{Rails.root}\/?/, '') end + + private + def output_inline? + options.fetch(:output_inline, true) + end + + def fail_fast? + options[:fail_fast] + end + + def format_rerun_snippet(result) + location, line = result.method(result.name).source_location + "#{self.executable} #{relative_path_for(location)}:#{line}" + end end end diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index fef65adda2..8b83784ed6 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -7,7 +7,10 @@ module ApplicationTests include Rack::Test::Methods def setup - build_app(initializers: true) + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + build_app(initializers: true) + end app_file "app/assets/javascripts/application.js", "//= require_tree ." app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" @@ -33,12 +36,19 @@ module ApplicationTests teardown_app end + # FIXME: shush Sass warning spam, not relevant to testing Railties + def get(*) + Kernel.silence_warnings { super } + end + test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do # config.assets.debug and config.assets.compile are false for production environment ENV["RAILS_ENV"] = "production" output = Dir.chdir(app_path){ `bin/rake assets:precompile --trace 2>&1` } assert $?.success?, output - require "#{app_path}/config/environment" + + # Load app env + app "production" class ::PostsController < ActionController::Base ; end @@ -51,8 +61,8 @@ module ApplicationTests test "assets are served with sourcemaps when compile is true and debug_assets params is true" do add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index ddcd51f3fc..dca5cf2e5b 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -17,14 +17,23 @@ module ApplicationTests end def precompile!(env = nil) - quietly do - precompile_task = "bin/rake assets:precompile #{env} --trace 2>&1" - output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } - assert $?.success?, output - output + with_env env.to_h do + quietly do + precompile_task = "bin/rake assets:precompile --trace 2>&1" + output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } + assert $?.success?, output + output + end end end + def with_env(env) + env.each { |k, v| ENV[k.to_s] = v } + yield + ensure + env.each_key { |k| ENV.delete k.to_s } + end + def clean_assets! quietly do assert Dir.chdir(app_path) { system('bin/rake assets:clobber') } @@ -32,7 +41,8 @@ module ApplicationTests end def assert_file_exists(filename) - assert Dir[filename].first, "missing #{filename}" + globbed = Dir[filename] + assert globbed.one?, "Found #{globbed.size} files matching #{filename}. All files in the directory: #{Dir.entries(File.dirname(filename)).inspect}" end def assert_no_file_exists(filename) @@ -51,7 +61,10 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end get "/assets/demo.js" assert_equal 'a = "/assets/rails.png";', last_response.body.strip @@ -62,8 +75,8 @@ module ApplicationTests add_to_env_config "production", "config.assets.compile = true" add_to_env_config "production", "config.assets.precompile = []" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" assert !defined?(Uglifier) get "/assets/demo.js" @@ -72,11 +85,10 @@ module ApplicationTests end test "precompile creates the file, gives it the original asset's content and run in production as default" do - app_file "app/assets/manifest.js", "//= link_tree ./javascripts" + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" - ENV["RAILS_ENV"] = nil precompile! files = Dir["#{app_path}/public/assets/application-*.js"] @@ -88,7 +100,7 @@ module ApplicationTests end def test_precompile_does_not_hit_the_database - app_file "app/assets/manifest.js", "//= link_tree ./javascripts" + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" app_file "app/controllers/users_controller.rb", <<-eoruby @@ -98,10 +110,9 @@ module ApplicationTests class User < ActiveRecord::Base; raise 'should not be reached'; end eoruby - ENV['RAILS_ENV'] = 'production' - ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname' - - precompile! + precompile! \ + RAILS_ENV: 'production', + DATABASE_URL: 'postgresql://baduser:badpass@127.0.0.1/dbname' files = Dir["#{app_path}/public/assets/application-*.js"] files << Dir["#{app_path}/public/assets/foo/application-*.js"].first @@ -109,9 +120,6 @@ module ApplicationTests assert_not_nil file, "Expected application.js asset to be generated, but none found" assert_equal "alert();".strip, File.read(file).strip end - ensure - ENV.delete 'RAILS_ENV' - ENV.delete 'DATABASE_URL' end test "precompile application.js and application.css and all other non JS/CSS files" do @@ -171,10 +179,9 @@ module ApplicationTests test 'precompile use assets defined in app env config' do add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]' - app_file 'app/assets/javascripts/something.js.erb', 'alert();' - precompile! 'RAILS_ENV=production' + precompile! RAILS_ENV: 'production' assert_file_exists("#{app_path}/public/assets/something-*.js") end @@ -183,15 +190,17 @@ module ApplicationTests add_to_config 'config.assets.precompile = [ "something_manifest.js" ]' add_to_env_config 'production', 'config.assets.precompile += [ "another_manifest.js" ]' - app_file 'app/assets/something_manifest.js', '//= link ./javascripts/something.js' - app_file 'app/assets/another_manifest.js', '//= link ./javascripts/another.js' + app_file 'app/assets/config/something_manifest.js', '//= link something.js' + app_file 'app/assets/config/another_manifest.js', '//= link another.js' app_file 'app/assets/javascripts/something.js.erb', 'alert();' app_file 'app/assets/javascripts/another.js.erb', 'alert();' - precompile! 'RAILS_ENV=production' + precompile! RAILS_ENV: 'production' + assert_file_exists("#{app_path}/public/assets/something_manifest-*.js") assert_file_exists("#{app_path}/public/assets/something-*.js") + assert_file_exists("#{app_path}/public/assets/another_manifest-*.js") assert_file_exists("#{app_path}/public/assets/another-*.js") end @@ -199,8 +208,8 @@ module ApplicationTests add_to_config "config.action_controller.perform_caching = false" add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" assert_equal Sprockets::CachedEnvironment, Rails.application.assets.class end @@ -211,8 +220,8 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"]) @@ -233,14 +242,14 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" add_to_env_config "production", "config.serve_static_files = true" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] - require "#{app_path}/config/environment" + # Load app env + app "production" # Checking if Uglifier is defined we can know if Sprockets was reached or not assert !defined?(Uglifier) @@ -249,12 +258,11 @@ module ApplicationTests assert !defined?(Uglifier) end - test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do + test "precompile properly refers files referenced with asset_path" do app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - add_to_env_config "test", "config.assets.digest = true" - precompile!('RAILS_ENV=test') + precompile! file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -265,8 +273,7 @@ module ApplicationTests app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) @@ -275,8 +282,8 @@ module ApplicationTests app_file "app/assets/images/rails.png", "p { url: change }" precompile! - assets = ActiveSupport::JSON.decode(File.read(manifest)) + assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_not_equal asset_path, assets["assets"]["application.css"] end @@ -284,8 +291,7 @@ module ApplicationTests app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -294,7 +300,7 @@ module ApplicationTests test "precompile should handle utf8 filenames" do filename = "レイルズ.png" app_file "app/assets/images/#{filename}", "not an image really" - app_file "app/assets/manifest.js", "//= link_tree ./images" + app_file "app/assets/config/manifest.js", "//= link_tree ../images" add_to_config "config.assets.precompile = %w(manifest.js)" precompile! @@ -303,7 +309,8 @@ module ApplicationTests assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/#{URI.parser.escape(asset_path)}" assert_match "not an image really", last_response.body @@ -326,8 +333,8 @@ module ApplicationTests app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" add_to_config "config.assets.compile = false" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" get "/assets/demo.js" assert_equal 404, last_response.status @@ -344,7 +351,8 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" class ::OmgController < ActionController::Base def index @@ -370,7 +378,8 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/demo.js" assert_match "alert();", last_response.body @@ -381,10 +390,10 @@ module ApplicationTests app_with_assets_in_view # config.assets.debug and config.assets.compile are false for production environment - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: 'production' - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end @@ -400,6 +409,7 @@ module ApplicationTests app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>" precompile! + assert_equal "Post\n;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first) end @@ -441,7 +451,10 @@ module ApplicationTests app_with_assets_in_view add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + + # Load app env + app "development" + class ::PostsController < ActionController::Base; end get '/posts', {}, {'HTTPS'=>'off'} @@ -456,6 +469,7 @@ module ApplicationTests add_to_config "config.assets.precompile = %w{rails.png image_loader.js}" add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" + precompile! assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first) @@ -467,6 +481,7 @@ module ApplicationTests app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png app.js}" add_to_env_config "development", "config.assets.digest = false" + precompile! assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 464fc1688c..2f407cd851 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -34,8 +34,19 @@ module ApplicationTests FileUtils.cp_r(app_path, new_app) end - def app - @app ||= Rails.application + def app(env = 'development') + @app ||= begin + ENV['RAILS_ENV'] = env + + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end + + Rails.application + ensure + ENV.delete 'RAILS_ENV' + end end def setup @@ -78,7 +89,9 @@ module ApplicationTests require 'my_logger' config.logger = MyLogger.new STDOUT RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_equal 'MyLogger', Rails.application.config.logger.class.name end @@ -97,7 +110,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"] @@ -128,29 +141,29 @@ module ApplicationTests test "Rails.application is nil until app is initialized" do require 'rails' assert_nil Rails.application - require "#{app_path}/config/environment" + app 'development' assert_equal AppTemplate::Application.instance, Rails.application end test "Rails.application responds to all instance methods" do - require "#{app_path}/config/environment" + app 'development' assert_respond_to Rails.application, :routes_reloader assert_equal Rails.application.routes_reloader, AppTemplate::Application.routes_reloader end test "Rails::Application responds to paths" do - require "#{app_path}/config/environment" + app 'development' assert_respond_to AppTemplate::Application, :paths assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded end test "the application root is set correctly" do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new(app_path), Rails.application.root end test "the application root can be seen from the application singleton" do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new(app_path), AppTemplate::Application.root end @@ -162,7 +175,8 @@ module ApplicationTests use_frameworks [] - require "#{app_path}/config/environment" + app 'development' + assert_equal Pathname.new(new_app), Rails.application.root end @@ -172,7 +186,7 @@ module ApplicationTests use_frameworks [] Dir.chdir("#{app_path}") do - require "#{app_path}/config/environment" + app 'development' assert_equal Pathname.new("#{app_path}"), Rails.application.root end end @@ -181,7 +195,9 @@ module ApplicationTests add_to_config <<-RUBY config.root = "#{app_path}" RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_instance_of Pathname, Rails.root end @@ -189,7 +205,9 @@ module ApplicationTests add_to_config <<-RUBY config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_instance_of Pathname, Rails.public_path end @@ -199,12 +217,13 @@ module ApplicationTests config.cache_classes = true RUBY - require "#{app_path}/config/application" - assert Rails.application.initialize! + app 'development' + + assert_equal :require, ActiveSupport::Dependencies.mechanism end test "application is always added to eager_load namespaces" do - require "#{app_path}/config/application" + app 'development' assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application end @@ -218,7 +237,7 @@ module ApplicationTests use_frameworks [] assert_nothing_raised do - require "#{app_path}/config/application" + app 'development' end end @@ -230,7 +249,7 @@ module ApplicationTests RUBY assert_nothing_raised do - require "#{app_path}/config/application" + app 'development' end end @@ -239,7 +258,7 @@ module ApplicationTests Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ] RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter'] end @@ -255,7 +274,7 @@ module ApplicationTests assert !$prepared - require "#{app_path}/config/environment" + app 'development' get "/" assert $prepared @@ -267,7 +286,7 @@ module ApplicationTests end test "skipping config.encoding still results in 'utf-8' as the default" do - require "#{app_path}/config/application" + app 'development' assert_utf8 end @@ -276,7 +295,7 @@ module ApplicationTests config.encoding = "utf-8" RUBY - require "#{app_path}/config/application" + app 'development' assert_utf8 end @@ -285,7 +304,7 @@ module ApplicationTests config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/application" + app 'development' assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path end @@ -293,7 +312,7 @@ module ApplicationTests restore_default_config with_rails_env "production" do - require "#{app_path}/config/environment" + app 'production' assert_not app.config.serve_static_files end end @@ -303,7 +322,7 @@ module ApplicationTests with_rails_env "production" do switch_env "RAILS_SERVE_STATIC_FILES", "1" do - require "#{app_path}/config/environment" + app 'production' assert app.config.serve_static_files end end @@ -314,7 +333,7 @@ module ApplicationTests with_rails_env "production" do switch_env "RAILS_SERVE_STATIC_FILES", " " do - require "#{app_path}/config/environment" + app 'production' assert_not app.config.serve_static_files end end @@ -363,8 +382,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + app 'development' assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator @@ -380,7 +399,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_deprecated(/You didn't set `secret_key_base`./) do app.env_config @@ -395,7 +415,8 @@ module ApplicationTests development: secret_token: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token end @@ -426,7 +447,7 @@ module ApplicationTests secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base end @@ -436,7 +457,7 @@ module ApplicationTests Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3" RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base end @@ -449,7 +470,8 @@ module ApplicationTests secret_key_base: secret_token: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token @@ -463,7 +485,8 @@ module ApplicationTests aws_secret_access_key: myamazonsecretaccesskey YAML - require "#{app_path}/config/environment" + app 'development' + assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key end @@ -471,7 +494,8 @@ module ApplicationTests test "blank config/secrets.yml does not crash the loading process" do app_file 'config/secrets.yml', <<-YAML YAML - require "#{app_path}/config/environment" + + app 'development' assert_nil app.secrets.not_defined end @@ -484,7 +508,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base end @@ -497,7 +522,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token assert_equal nil, app.secrets.secret_key_base @@ -512,7 +538,8 @@ module ApplicationTests development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app 'development' assert_equal '', app.config.secret_token assert_equal nil, app.secrets.secret_key_base @@ -535,7 +562,6 @@ module ApplicationTests end test "default form builder specified as a string" do - app_file 'config/initializers/form_builder.rb', <<-RUBY class CustomFormBuilder < ActionView::Helpers::FormBuilder def text_field(attribute, *args) @@ -567,7 +593,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' get "/posts" assert_match(/label/, last_response.body) @@ -606,9 +632,9 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' - params = {authenticity_token: token} + params = { authenticity_token: token } get "/posts/1" assert_match(/patch/, last_response.body) @@ -659,9 +685,9 @@ module ApplicationTests config.action_mailer.interceptors = MyMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") @@ -672,9 +698,9 @@ module ApplicationTests config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"] RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") @@ -685,9 +711,9 @@ module ApplicationTests config.action_mailer.preview_interceptors = MyPreviewMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors @@ -698,9 +724,9 @@ module ApplicationTests config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"] RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors @@ -711,9 +737,9 @@ module ApplicationTests ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [], ActionMailer::Base.preview_interceptors @@ -724,9 +750,9 @@ module ApplicationTests config.action_mailer.observers = MyMailObserver RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") @@ -737,9 +763,9 @@ module ApplicationTests config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"] RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") @@ -750,9 +776,9 @@ module ApplicationTests config.action_mailer.deliver_later_queue_name = 'test_default' RUBY - require "#{app_path}/config/environment" - require "mail" + app 'development' + require "mail" _ = ActionMailer::Base assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name") @@ -764,7 +790,7 @@ module ApplicationTests config.time_zone = "Wellington" RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal "Wellington", Rails.application.config.time_zone end @@ -776,7 +802,7 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app 'development' end end @@ -786,7 +812,7 @@ module ApplicationTests config.beginning_of_week = :wednesday RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :wednesday, Rails.application.config.beginning_of_week end @@ -798,13 +824,14 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app 'development' end end test "config.action_view.cache_template_loading with cache_classes default" do add_to_config "config.cache_classes = true" - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal true, ActionView::Resolver.caching? @@ -812,7 +839,8 @@ module ApplicationTests test "config.action_view.cache_template_loading without cache_classes default" do add_to_config "config.cache_classes = false" - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal false, ActionView::Resolver.caching? @@ -823,7 +851,8 @@ module ApplicationTests config.cache_classes = true config.action_view.cache_template_loading = false RUBY - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal false, ActionView::Resolver.caching? @@ -834,7 +863,8 @@ module ApplicationTests config.cache_classes = false config.action_view.cache_template_loading = true RUBY - require "#{app_path}/config/environment" + + app 'development' require 'action_view/base' assert_equal true, ActionView::Resolver.caching? @@ -849,7 +879,7 @@ module ApplicationTests require 'action_view/railtie' require 'action_view/base' - require "#{app_path}/config/environment" + app 'development' assert_equal false, ActionView::Resolver.caching? end @@ -902,7 +932,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json" assert_equal '{"title"=>"foo"}', last_response.body @@ -924,7 +954,7 @@ module ApplicationTests config.action_controller.permit_all_parameters = true RUBY - require "#{app_path}/config/environment" + app 'development' post "/posts", {post: {"title" =>"zomg"}} assert_equal 'permitted', last_response.body @@ -946,7 +976,7 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters @@ -955,7 +985,7 @@ module ApplicationTests end test "config.action_controller.always_permitted_parameters are: controller, action by default" do - require "#{app_path}/config/environment" + app 'development' assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters end @@ -963,7 +993,9 @@ module ApplicationTests add_to_config <<-RUBY config.action_controller.always_permitted_parameters = %w( controller action format ) RUBY - require "#{app_path}/config/environment" + + app 'development' + assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters end @@ -984,7 +1016,7 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters @@ -993,25 +1025,19 @@ module ApplicationTests end test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do - ENV["RAILS_ENV"] = "development" - - require "#{app_path}/config/environment" + app 'development' assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do - ENV["RAILS_ENV"] = "test" - - require "#{app_path}/config/environment" + app 'test' assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do - ENV["RAILS_ENV"] = "production" - - require "#{app_path}/config/environment" + app 'production' assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters end @@ -1089,17 +1115,14 @@ module ApplicationTests test "config.active_record.dump_schema_after_migration is false on production" do build_app - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + app 'production' assert_not ActiveRecord::Base.dump_schema_after_migration end test "config.active_record.dump_schema_after_migration is true by default on development" do - ENV["RAILS_ENV"] = "development" - - require "#{app_path}/config/environment" + app 'development' assert ActiveRecord::Base.dump_schema_after_migration end @@ -1125,7 +1148,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block require 'rake' @@ -1147,7 +1170,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_generators @@ -1165,7 +1188,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_console @@ -1183,7 +1206,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_not Rails.configuration.ran_block Rails.application.load_runner @@ -1199,14 +1222,14 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app 'development' assert_kind_of Hash, Rails.application.config.database_configuration end test 'raises with proper error message if no database configuration found' do FileUtils.rm("#{app_path}/config/database.yml") - require "#{app_path}/config/environment" + app 'development' err = assert_raises RuntimeError do Rails.application.config.database_configuration end @@ -1214,25 +1237,23 @@ module ApplicationTests end test 'config.action_mailer.show_previews defaults to true in development' do - Rails.env = "development" - require "#{app_path}/config/environment" + app 'development' assert Rails.application.config.action_mailer.show_previews end test 'config.action_mailer.show_previews defaults to false in production' do - Rails.env = "production" - require "#{app_path}/config/environment" + app 'production' assert_equal false, Rails.application.config.action_mailer.show_previews end test 'config.action_mailer.show_previews can be set in the configuration file' do - Rails.env = "production" add_to_config <<-RUBY config.action_mailer.show_previews = true RUBY - require "#{app_path}/config/environment" + + app 'production' assert_equal true, Rails.application.config.action_mailer.show_previews end @@ -1247,7 +1268,7 @@ module ApplicationTests config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + app 'development' assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end @@ -1258,7 +1279,7 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app 'development' end assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message @@ -1273,7 +1294,8 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal({}, Rails.application.config.my_custom_config) end @@ -1285,7 +1307,8 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal({}, Rails.application.config.my_custom_config) end @@ -1299,7 +1322,8 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app 'development' assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end @@ -1315,7 +1339,7 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app 'development' end assert_match 'YAML syntax error occurred while parsing', exception.message diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 2d47a31826..acfba21f1c 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -340,6 +340,36 @@ module ApplicationTests assert_match '0 runs, 0 assertions', run_test_command('') end + def test_output_inline_by_default + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + assert false, 'wups!' + end + end + RUBY + + output = run_test_command('test/models/post_test.rb') + assert_match %r{Running:\n\nF\n\nwups!\n\nbin/rails test test/models/post_test.rb:4}, output + end + + def test_fail_fast + app_file 'test/models/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + assert false, 'wups!' + end + end + RUBY + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command('test/models/post_test.rb --fail-fast') }) + end + def test_raise_error_when_specified_file_does_not_exist error = capture(:stderr) { run_test_command('test/not_exists.rb') } assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 41d36680e0..df3c2ca66d 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -51,7 +51,12 @@ module TestHelpers old_env = ENV["RAILS_ENV"] @app ||= begin ENV["RAILS_ENV"] = env - require "#{app_path}/config/environment" + + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require "#{app_path}/config/environment" + end + Rails.application end ensure @@ -161,18 +166,18 @@ module TestHelpers require "action_view/railtie" require 'action_dispatch/middleware/flash' - app = Class.new(Rails::Application) - app.config.eager_load = false - app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" - app.config.session_store :cookie_store, key: "_myapp_session" - app.config.active_support.deprecation = :log - app.config.active_support.test_order = :random - app.config.log_level = :info + @app = Class.new(Rails::Application) + @app.config.eager_load = false + @app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" + @app.config.session_store :cookie_store, key: "_myapp_session" + @app.config.active_support.deprecation = :log + @app.config.active_support.test_order = :random + @app.config.log_level = :info - yield app if block_given? - app.initialize! + yield @app if block_given? + @app.initialize! - app.routes.draw do + @app.routes.draw do get "/" => "omg#index" end @@ -297,7 +302,10 @@ module TestHelpers end def boot_rails - require File.expand_path('../../../../load_paths', __FILE__) + # FIXME: shush Sass warning spam, not relevant to testing Railties + Kernel.silence_warnings do + require File.expand_path('../../../../load_paths', __FILE__) + end end end end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index 3066ba82d6..59fdf4bc36 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -57,6 +57,59 @@ class TestUnitReporterTest < ActiveSupport::TestCase end end + test "outputs failures inline" do + @reporter.record(failed_test) + @reporter.report + + assert_match %r{\A\n\nboo\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string + end + + test "outputs errors inline" do + @reporter.record(errored_test) + @reporter.report + + assert_match %r{\A\n\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string + end + + test "outputs skipped tests inline if verbose" do + verbose = Rails::TestUnitReporter.new @output, verbose: true + verbose.record(skipped_test) + verbose.report + + assert_match %r{\A\n\nskipchurches, misstemples\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string + end + + test "does not output rerun snippets after run" do + @reporter.record(failed_test) + @reporter.report + + assert_no_match 'Failed tests:', @output.string + end + + test "fail fast interrupts run on failure" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false + + # Minitest passes through Interrupt, catch it manually. + begin + fail_fast.record(failed_test) + rescue Interrupt + interrupt_raised = true + ensure + assert interrupt_raised, 'Expected Interrupt to be raised.' + end + end + + test "fail fast does not interrupt run errors or skips" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + + fail_fast.record(errored_test) + assert_no_match 'Failed tests:', @output.string + + fail_fast.record(skipped_test) + assert_no_match 'Failed tests:', @output.string + end + private def assert_rerun_snippet_count(snippet_count) assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size @@ -72,6 +125,12 @@ class TestUnitReporterTest < ActiveSupport::TestCase ft end + def errored_test + et = ExampleTest.new(:woot) + et.failures << Minitest::UnexpectedError.new(ArgumentError.new("wups")) + et + end + def passing_test ExampleTest.new(:woot) end @@ -79,7 +138,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase def skipped_test st = ExampleTest.new(:woot) st.failures << begin - raise Minitest::Skip + raise Minitest::Skip, "skipchurches, misstemples" rescue Minitest::Assertion => e e end |