diff options
153 files changed, 2469 insertions, 919 deletions
diff --git a/.gitignore b/.gitignore index 4961ad588f..32939b7bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ pkg /railties/doc /railties/tmp /guides/output +node_modules/ +/actionview/log diff --git a/.travis.yml b/.travis.yml index eafa06e44f..2006291052 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ cache: directories: - /tmp/cache/unicode_conformance - /tmp/beanstalkd-1.10 + - node_modules + - $HOME/.nvm services: - memcached @@ -21,6 +23,11 @@ before_install: - "gem update bundler" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" + - "[[ $GEM != 'av:ujs' ]] || nvm install node" + - "[[ $GEM != 'av:ujs' ]] || node --version" + - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)" + - "[[ $GEM != 'av:ujs' ]] || [[ $(phantomjs --version) > '2' ]] || npm install -g phantomjs-prebuilt" + before_script: # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted. @@ -52,6 +59,8 @@ rvm: matrix: include: + - rvm: 2.4.0 + env: "GEM=av:ujs" - rvm: 2.2.6 env: "GEM=aj:integration" services: @@ -91,7 +100,7 @@ matrix: - rvm: jruby-9.1.7.0 jdk: oraclejdk8 env: - - "GEM=am,aj" + - "GEM=am,amo,aj" allow_failures: - rvm: ruby-head - rvm: jruby-9.1.7.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6ebef7e89..b44486c75a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,9 @@ #### **Did you find a bug?** +* **Do not open up a GitHub issue if the bug is a security vulnerability + in Rails**, and instead to refer to our [security policy](http://rubyonrails.org/security/). + * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues). * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. diff --git a/Gemfile.lock b/Gemfile.lock index ffecf4c519..74f76f9e7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,58 +26,58 @@ GIT PATH remote: . specs: - actioncable (5.1.0.alpha) - actionpack (= 5.1.0.alpha) + actioncable (5.1.0.beta1) + actionpack (= 5.1.0.beta1) nio4r (~> 2.0) websocket-driver (~> 0.6.1) - actionmailer (5.1.0.alpha) - actionpack (= 5.1.0.alpha) - actionview (= 5.1.0.alpha) - activejob (= 5.1.0.alpha) + actionmailer (5.1.0.beta1) + actionpack (= 5.1.0.beta1) + actionview (= 5.1.0.beta1) + activejob (= 5.1.0.beta1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.0.alpha) - actionview (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + actionpack (5.1.0.beta1) + actionview (= 5.1.0.beta1) + activesupport (= 5.1.0.beta1) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.0.alpha) - activesupport (= 5.1.0.alpha) + actionview (5.1.0.beta1) + activesupport (= 5.1.0.beta1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.1.0.alpha) - activesupport (= 5.1.0.alpha) + activejob (5.1.0.beta1) + activesupport (= 5.1.0.beta1) globalid (>= 0.3.6) - activemodel (5.1.0.alpha) - activesupport (= 5.1.0.alpha) - activerecord (5.1.0.alpha) - activemodel (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + activemodel (5.1.0.beta1) + activesupport (= 5.1.0.beta1) + activerecord (5.1.0.beta1) + activemodel (= 5.1.0.beta1) + activesupport (= 5.1.0.beta1) arel (~> 8.0) - activesupport (5.1.0.alpha) + activesupport (5.1.0.beta1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - rails (5.1.0.alpha) - actioncable (= 5.1.0.alpha) - actionmailer (= 5.1.0.alpha) - actionpack (= 5.1.0.alpha) - actionview (= 5.1.0.alpha) - activejob (= 5.1.0.alpha) - activemodel (= 5.1.0.alpha) - activerecord (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + rails (5.1.0.beta1) + actioncable (= 5.1.0.beta1) + actionmailer (= 5.1.0.beta1) + actionpack (= 5.1.0.beta1) + actionview (= 5.1.0.beta1) + activejob (= 5.1.0.beta1) + activemodel (= 5.1.0.beta1) + activerecord (= 5.1.0.beta1) + activesupport (= 5.1.0.beta1) bundler (>= 1.3.0, < 2.0) - railties (= 5.1.0.alpha) + railties (= 5.1.0.beta1) sprockets-rails (>= 2.0.0) - railties (5.1.0.alpha) - actionpack (= 5.1.0.alpha) - activesupport (= 5.1.0.alpha) + railties (5.1.0.beta1) + actionpack (= 5.1.0.beta1) + activesupport (= 5.1.0.beta1) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -186,7 +186,7 @@ GEM activesupport (>= 4.1.0) hiredis (0.6.1) http_parser.rb (0.6.0) - i18n (0.8.0) + i18n (0.8.1) jquery-rails (4.2.2) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) diff --git a/RAILS_VERSION b/RAILS_VERSION index 8ea1016081..d5d15fa148 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.1.0.alpha +5.1.0.beta1 @@ -71,13 +71,17 @@ and may also be used independently outside Rails. * [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html) * [Ruby on Rails Guides](http://guides.rubyonrails.org) * [The API Documentation](http://api.rubyonrails.org) - * [Ruby on Rails Tutorial](http://www.railstutorial.org/book) + * [Ruby on Rails Tutorial](https://www.railstutorial.org/book) ## Contributing We encourage you to contribute to Ruby on Rails! Please check out the [Contributing to Ruby on Rails guide](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](http://contributors.rubyonrails.org) +Trying to report a possible security vulnerability in Rails? Please +check out our [security policy](http://rubyonrails.org/security/) for +guidelines about how to proceed. + Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). ## Code Status diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 7657a05077..a0254fe323 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Redis subscription adapters now support `channel_prefix` option in `cable.yml` Avoids channel name collisions when multiple apps use the same Redis server. diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index 8ba0230d47..c09613a747 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -8,7 +8,7 @@ module ActionCable MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb index 56b068976b..ed8f315791 100644 --- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb @@ -24,6 +24,12 @@ module ActionCable cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } def initialize(*) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The "evented_redis" subscription adapter is deprecated and + will be removed in Rails 5.2. Please use the "redis" adapter + instead. + MSG + super @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil end diff --git a/actioncable/package.json b/actioncable/package.json index 37f82fa1ea..69ae3519d9 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,6 +1,6 @@ { "name": "actioncable", - "version": "5.0.0-rc1", + "version": "5.1.0-beta1", "description": "WebSocket framework for Ruby on Rails.", "main": "lib/assets/compiled/action_cable.js", "files": [ diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb index c55d35848e..256458bc24 100644 --- a/actioncable/test/subscription_adapter/evented_redis_test.rb +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -7,7 +7,9 @@ class EventedRedisAdapterTest < ActionCable::TestCase include ChannelPrefixTest def setup - super + assert_deprecated do + super + end # em-hiredis is warning-rich @previous_verbose, $VERBOSE = $VERBOSE, nil diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 4f99bb1b7a..ee33450b45 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Add `:args` to `process.action_mailer` event. *Yuji Yaginuma* diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 7dafceef2b..de2d71bd3e 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -8,7 +8,7 @@ module ActionMailer MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 0167dcbf96..6bb1c63610 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,12 @@ +* Commit flash changes when using a redirect route. + + Fixes #27992. + + *Andrew White* + + +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Prefer `remove_method` over `undef_method` when reloading routes When `undef_method` is used it prevents access to other implementations of that @@ -11,7 +20,7 @@ ``` ruby resource :basket - resolve(class: "Basket") { [:basket] } + resolve("Basket") { [:basket] } ``` ``` erb diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 073dabd0a8..6e06c70dc2 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1904,7 +1904,7 @@ module ActionDispatch ast = Journey::Parser.parse path mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) - @set.add_route(mapping, ast, as, anchor) + @set.add_route(mapping, as) end def match_root_route(options) @@ -2034,7 +2034,7 @@ module ActionDispatch # end # # direct :main do - # { controller: 'pages', action: 'index', subdomain: 'www' } + # { controller: "pages", action: "index", subdomain: "www" } # end # # The return value from the block passed to `direct` must be a valid set of @@ -2042,7 +2042,7 @@ module ActionDispatch # be one of the following: # # * A string, which is treated as a generated url - # * A hash, e.g. { controller: 'pages', action: 'index' } + # * A hash, e.g. { controller: "pages", action: "index" } # * An array, which is passed to `polymorphic_url` # * An Active Model instance # * An Active Model class @@ -2057,6 +2057,15 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size)) ] # end # + # In this instance the `params` object comes from the context in which the the + # block is executed, e.g. generating a url inside a controller action or a view. + # If the block is executed where there isn't a params object such as this: + # + # Rails.application.routes.url_helpers.browse_path + # + # then it will raise a `NameError`. Because of this you need to be aware of the + # context in which you will use your custom url helper when defining it. + # # NOTE: The `direct` method can't be used inside of a scope block such as # `namespace` or `scope` and will raise an error if it detects that it is. def direct(name, options = {}, &block) @@ -2101,7 +2110,7 @@ module ActionDispatch # You can pass options to a polymorphic mapping - the arity for the block # needs to be two as the instance is passed as the first argument, e.g: # - # direct class: "Basket", anchor: "items" do |basket, options| + # resolve "Basket", anchor: "items" do |basket, options| # [:basket, options] # end # diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index dabc045007..e8f47b8640 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -36,6 +36,8 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? + req.commit_flash + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2672cd24ed..c4719f8a71 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -565,7 +565,7 @@ module ActionDispatch routes.empty? end - def add_route(mapping, path_ast, name, anchor) + def add_route(mapping, name) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 99c2be0a35..d6388d8fb7 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -7,79 +7,79 @@ require "action_dispatch/system_testing/test_helpers/screenshot_helper" require "action_dispatch/system_testing/test_helpers/setup_and_teardown" module ActionDispatch + # = System Testing + # + # System tests let you test applications in the browser. Because system + # tests use a real browser experience, you can test all of your JavaScript + # easily from your test suite. + # + # To create a system test in your application, extend your test class + # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a + # base and allow you to configure the settings through your + # <tt>application_system_test_case.rb</tt> file that is generated with a new + # application or scaffold. + # + # Here is an example system test: + # + # require 'application_system_test_case' + # + # class Users::CreateTest < ApplicationSystemTestCase + # test "adding a new user" do + # visit users_path + # click_on 'New User' + # + # fill_in 'Name', with: 'Arya' + # click_on 'Create User' + # + # assert_text 'Arya' + # end + # end + # + # When generating an application or scaffold, an +application_system_test_case.rb+ + # file will also be generated containing the base class for system testing. + # This is where you can change the driver, add Capybara settings, and other + # configuration for your system tests. + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + # end + # + # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the + # Selenium driver, with the Chrome browser, and a browser size of 1400x1400. + # + # Changing the driver configuration options are easy. Let's say you want to use + # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+ + # file add the following: + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :firefox + # end + # + # +driven_by+ has a required argument for the driver name. The keyword + # arguments are +:using+ for the browser and +:screen_size+ to change the + # size of the browser screen. These two options are not applicable for + # headless drivers and will be silently ignored if passed. + # + # To use a headless driver, like Poltergeist, update your Gemfile to use + # Poltergeist instead of Selenium and then declare the driver name in the + # +application_system_test_case.rb+ file. In this case you would leave out the +:using+ + # option because the driver is headless. + # + # require "test_helper" + # require "capybara/poltergeist" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :poltergeist + # end + # + # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara + # and Rails, any driver that is supported by Capybara is supported by system + # tests as long as you include the required gems and files. class SystemTestCase < IntegrationTest - # = System Testing - # - # System tests let you test applications in the browser. Because system - # tests use a real browser experience, you can test all of your JavaScript - # easily from your test suite. - # - # To create a system test in your application, extend your test class - # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a - # base and allow you to configure the settings through your - # <tt>application_system_test_case.rb</tt> file that is generated with a new - # application or scaffold. - # - # Here is an example system test: - # - # require 'application_system_test_case' - # - # class Users::CreateTest < ApplicationSystemTestCase - # test "adding a new user" do - # visit users_path - # click_on 'New User' - # - # fill_in 'Name', with: 'Arya' - # click_on 'Create User' - # - # assert_text 'Arya' - # end - # end - # - # When generating an application or scaffold, an +application_system_test_case.rb+ - # file will also be generated containing the base class for system testing. - # This is where you can change the driver, add Capybara settings, and other - # configuration for your system tests. - # - # require "test_helper" - # - # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - # driven_by :selenium, using: :chrome, screen_size: [1400, 1400] - # end - # - # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the - # Selenium driver, with the Chrome browser, and a browser size of 1400x1400. - # - # Changing the driver configuration options are easy. Let's say you want to use - # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+ - # file add the following: - # - # require "test_helper" - # - # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - # driven_by :selenium, using: :firefox - # end - # - # +driven_by+ has a required argument for the driver name. The keyword - # arguments are +:using+ for the browser and +:screen_size+ to change the - # size of the browser screen. These two options are not applicable for - # headless drivers and will be silently ignored if passed. - # - # To use a headless driver, like Poltergeist, update your Gemfile to use - # Poltergeist instead of Selenium and then declare the driver name in the - # +application_system_test_case.rb+ file. In this case you would leave out the +:using+ - # option because the driver is headless. - # - # require "test_helper" - # require "capybara/poltergeist" - # - # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - # driven_by :poltergeist - # end - # - # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara - # and Rails, any driver that is supported by Capybara is supported by system - # tests as long as you include the required gems and files. include Capybara::DSL include SystemTesting::TestHelpers::SetupAndTeardown include SystemTesting::TestHelpers::ScreenshotHelper @@ -105,9 +105,16 @@ module ActionDispatch # # driven_by :selenium, screen_size: [800, 800] def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400]) - SystemTesting::Driver.new(driver).run + driver = if selenium?(driver) + SystemTesting::Browser.new(using, screen_size) + else + SystemTesting::Driver.new(driver) + end + + setup { driver.use } + teardown { driver.reset } + SystemTesting::Server.new.run - SystemTesting::Browser.new(using, screen_size).run if selenium?(driver) end def self.selenium?(driver) # :nodoc: diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index c9a6628516..14ea06459d 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -1,14 +1,17 @@ +require "action_dispatch/system_testing/driver" + module ActionDispatch module SystemTesting - class Browser # :nodoc: + class Browser < Driver # :nodoc: def initialize(name, screen_size) + super(name) @name = name @screen_size = screen_size end - def run + def use register - setup + super end private @@ -19,10 +22,6 @@ module ActionDispatch end end end - - def setup - Capybara.default_driver = @name.to_sym - end end end end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 7c2ad84e19..8decb54419 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -5,14 +5,14 @@ module ActionDispatch @name = name end - def run - register + def use + @current = Capybara.current_driver + Capybara.current_driver = @name end - private - def register - Capybara.default_driver = @name - end + def reset + Capybara.current_driver = @current + end end end end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index 784005cb93..ddc961cf84 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -22,12 +22,12 @@ module ActionDispatch # fails add +take_failed_screenshot+ to the teardown block before clearing # sessions. def take_failed_screenshot - take_screenshot unless passed? + take_screenshot if failed? end private def image_name - passed? ? method_name : "failures_#{method_name}" + failed? ? "failures_#{method_name}" : method_name end def image_path @@ -51,6 +51,10 @@ module ActionDispatch def inline_base64(path) Base64.encode64(path).gsub("\n", "") end + + def failed? + !passed? && !skipped? + end end end end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb index 491559eedf..1c89bfacfa 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -10,8 +10,8 @@ module ActionDispatch end def after_teardown - super take_failed_screenshot + super Capybara.reset_sessions! end end diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index d8f86630b1..d6a91a0569 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -8,7 +8,7 @@ module ActionPack MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb index 6d230a2557..f85b989892 100644 --- a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb +++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb @@ -96,6 +96,10 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest direct(:options) { |options| [:products, options] } direct(:defaults, size: 10) { |options| [:products, options] } + direct(:browse, page: 1, size: 10) do |options| + [:products, options.merge(params.permit(:page, :size).to_h.symbolize_keys)] + end + resolve("Article") { |article| [:post, { id: article.id }] } resolve("Basket") { |basket| [:basket] } resolve("User", anchor: "details") { |user, options| [:profile, options] } @@ -127,6 +131,10 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action) end + def params + ActionController::Parameters.new(page: 2, size: 25) + end + def test_direct_paths assert_equal "http://www.rubyonrails.org", website_path assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_path @@ -162,6 +170,9 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest assert_equal "/products?size=10", Routes.url_helpers.defaults_path assert_equal "/products?size=20", defaults_path(size: 20) assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20) + + assert_equal "/products?page=2&size=25", browse_path + assert_raises(NameError) { Routes.url_helpers.browse_path } end def test_direct_urls @@ -199,6 +210,9 @@ class TestCustomUrlHelpers < ActionDispatch::IntegrationTest assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20) assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20) + + assert_equal "http://www.example.com/products?page=2&size=25", browse_url + assert_raises(NameError) { Routes.url_helpers.browse_url } end def test_resolve_paths diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 53758a4fbc..d563df91df 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4913,3 +4913,52 @@ class TestInternalRoutingParams < ActionDispatch::IntegrationTest ) end end + +class FlashRedirectTest < ActionDispatch::IntegrationTest + SessionKey = "_myapp_session" + Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + + class KeyGeneratorMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.key_generator"] ||= Generator + @app.call(env) + end + end + + class FooController < ActionController::Base + def bar + render plain: (flash[:foo] || "foo") + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + get "/foo", to: redirect { |params, req| req.flash[:foo] = "bar"; "/bar" } + get "/bar", to: "flash_redirect_test/foo#bar" + end + + APP = build_app Routes do |middleware| + middleware.use KeyGeneratorMiddleware + middleware.use ActionDispatch::Session::CookieStore, key: SessionKey + middleware.use ActionDispatch::Flash + middleware.delete ActionDispatch::ShowExceptions + end + + def app + APP + end + + include Routes.url_helpers + + def test_block_redirect_commits_flash + get "/foo", env: { "action_dispatch.key_generator" => Generator } + assert_response :redirect + + follow_redirect! + assert_equal "bar", response.body + end +end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index bd8318f5f6..3082d1072b 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -224,7 +224,7 @@ module StaticTests def assert_gzip(file_name, response) expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name) - actual = Zlib::GzipReader.new(StringIO.new(response.body)).read + actual = ActiveSupport::Gzip.decompress(response.body) assert_equal expected, actual end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb index 8c14f799b0..3b4ea96c4f 100644 --- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -15,4 +15,14 @@ class ScreenshotHelperTest < ActiveSupport::TestCase assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path) end end + + test "image path does not include failures text if test skipped" do + new_test = ActionDispatch::SystemTestCase.new("x") + + new_test.stub :passed?, false do + new_test.stub :skipped?, true do + assert_equal "tmp/screenshots/x.png", new_test.send(:image_path) + end + end + end end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index a384902a14..ff01d6739a 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -1,21 +1,23 @@ require "abstract_unit" -class SystemTestCaseTest < ActiveSupport::TestCase - test "driven_by sets Capybara's default driver to poltergeist" do - ActionDispatch::SystemTestCase.driven_by :poltergeist - - assert_equal :poltergeist, Capybara.default_driver +class DrivenByCaseTestTest < ActiveSupport::TestCase + test "selenium? returns false if driver is poltergeist" do + assert_not ActionDispatch::SystemTestCase.selenium?(:poltergeist) end +end - test "driven_by sets Capybara's drivers respectively" do - ActionDispatch::SystemTestCase.driven_by :selenium, using: :chrome +class DrivenByRackTestTest < ActionDispatch::SystemTestCase + driven_by :rack_test - assert_includes Capybara.drivers, :selenium - assert_includes Capybara.drivers, :chrome - assert_equal :chrome, Capybara.default_driver + test "uses rack_test" do + assert_equal :rack_test, Capybara.current_driver end +end - test "selenium? returns false if driver is poltergeist" do - assert_not ActionDispatch::SystemTestCase.selenium?(:poltergeist) +class DrivenBySeleniumWithChromeTest < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome + + test "uses selenium" do + assert_equal :chrome, Capybara.current_driver end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index b071b260c9..f2ae74454a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,12 @@ +* Remove the option `encode_special_chars` misnomer from `strip_tags` + + As of rails-html-sanitizer v1.0.3 sanitizer will ignore the + `encode_special_chars` option. Fixes #28060. + + *Andrew Hood* + +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Change the ERB handler from Erubis to Erubi. Erubi is an Erubis fork that's svelte, simple, and currently maintained. diff --git a/actionview/Rakefile b/actionview/Rakefile index cba4684076..00ab92129d 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -1,4 +1,5 @@ require "rake/testtask" +require "fileutils" desc "Default Task" task default: :test @@ -25,6 +26,32 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end + task :ujs do + begin + Dir.mkdir("log") + pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1") + + start_time = Time.now + + loop do + break if system("lsof -i :4567 >/dev/null") + + if Time.now - start_time > 5 + puts "Timed out after 5 seconds" + exit 1 + end + end + + system("npm run lint && phantomjs ../ci/phantomjs.js http://localhost:4567/") + status = $?.to_i + ensure + Process.kill("KILL", pid) if pid + FileUtils.rm_f("log") + end + + exit status + end + namespace :integration do desc "ActiveRecord Integration Tests" Rake::TestTask.new(:active_record) do |t| diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE new file mode 100644 index 0000000000..befcbdc7b7 --- /dev/null +++ b/actionview/app/assets/javascripts/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2007-2017 Rails Core team + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md new file mode 100644 index 0000000000..92f3e8a3b3 --- /dev/null +++ b/actionview/app/assets/javascripts/README.md @@ -0,0 +1,49 @@ +Ruby on Rails unobtrusive scripting adapter. +======================================== + +This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to: + +- force confirmation dialogs for various actions; +- make non-GET requests from hyperlinks; +- make forms or hyperlinks submit data asynchronously with Ajax; +- have submit buttons become automatically disabled on form submit to prevent double-clicking. + +These features are achieved by adding certain ["data" attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers. + +Requirements +------------ + +- HTML5 doctype (optional). + +If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents. + +Installation using npm +------------ + +Run `npm install rails-ujs --save` to install the rails-ujs package. + +Installation using Yarn +------------ + +Run `yarn add rails-ujs` to install the rails-ujs package. + +Usage +------------ + +Require `rails-ujs` into your application.js manifest. + +```javascript +//= require rails-ujs +``` + +How to run tests +------------ + +Run `bundle exec rake ujs:server` first, and then run the web tests by visiting [[http://localhost:4567]] in your browser. + +## License +rails-ujs is released under the [MIT License](MIT-LICENSE). + +[data]: http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes "Embedding custom non-visible data with the data-* attributes" +[validator]: http://validator.w3.org/ +[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html diff --git a/actionview/coffeelint.json b/actionview/coffeelint.json new file mode 100644 index 0000000000..cf8bf2171b --- /dev/null +++ b/actionview/coffeelint.json @@ -0,0 +1,135 @@ +{ + "arrow_spacing": { + "level": "warn" + }, + "braces_spacing": { + "level": "warn", + "spaces": 1, + "empty_object_spaces": 0 + }, + "camel_case_classes": { + "level": "error" + }, + "coffeescript_error": { + "level": "error" + }, + "colon_assignment_spacing": { + "level": "warn", + "spacing": { + "left": 0, + "right": 1 + } + }, + "cyclomatic_complexity": { + "level": "warn", + "value": 10 + }, + "duplicate_key": { + "level": "error" + }, + "empty_constructor_needs_parens": { + "level": "warn" + }, + "ensure_comprehensions": { + "level": "warn" + }, + "eol_last": { + "level": "warn" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "line_endings": { + "level": "warn", + "value": "unix" + }, + "max_line_length": { + "value": 80, + "level": "ignore", + "limitComments": true + }, + "missing_fat_arrows": { + "level": "ignore" + }, + "newlines_after_classes": { + "value": 3, + "level": "warn" + }, + "no_backticks": { + "level": "error" + }, + "no_debugger": { + "level": "warn", + "console": false + }, + "no_empty_functions": { + "level": "warn" + }, + "no_empty_param_list": { + "level": "warn" + }, + "no_implicit_braces": { + "level": "ignore", + "strict": true + }, + "no_implicit_parens": { + "level": "ignore", + "strict": true + }, + "no_interpolation_in_single_quotes": { + "level": "warn" + }, + "no_nested_string_interpolation": { + "level": "warn" + }, + "no_plusplus": { + "level": "warn" + }, + "no_private_function_fat_arrows": { + "level": "warn" + }, + "no_stand_alone_at": { + "level": "warn" + }, + "no_tabs": { + "level": "error" + }, + "no_this": { + "level": "warn" + }, + "no_throwing_strings": { + "level": "error" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_trailing_whitespace": { + "level": "error", + "allowed_in_comments": false, + "allowed_in_empty_lines": true + }, + "no_unnecessary_double_quotes": { + "level": "warn" + }, + "no_unnecessary_fat_arrows": { + "level": "warn" + }, + "non_empty_constructor_needs_parens": { + "level": "warn" + }, + "prefer_english_operator": { + "level": "ignore", + "doubleNotLevel": "warn" + }, + "space_operators": { + "level": "warn" + }, + "spacing_after_comma": { + "level": "warn" + }, + "transform_messes_up_line_numbers": { + "level": "warn" + } +} + diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 5fc4f3f1b9..662a85f191 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -8,7 +8,7 @@ module ActionView MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 1e9b813d3d..0abd5bc5dc 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -13,6 +13,7 @@ module ActionView # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, # ASCII, and hex character references to work around these protocol filters. + # All special characters will be escaped. # # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information. @@ -20,8 +21,7 @@ module ActionView # Custom sanitization rules can also be provided. # # Please note that sanitizing user-provided text does not guarantee that the - # resulting markup is valid or even well-formed. For example, the output may still - # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>. + # resulting markup is valid or even well-formed. # # ==== Options # @@ -86,7 +86,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from +html+, including comments. + # Strips all HTML tags from +html+, including comments and special characters. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -96,8 +96,11 @@ module ActionView # # strip_tags("<div id='top-bar'>Welcome to my website!</div>") # # => Welcome to my website! + # + # strip_tags("> A quote from Smith & Wesson") + # # => > A quote from Smith & Wesson def strip_tags(html) - self.class.full_sanitizer.sanitize(html, encode_special_chars: false) + self.class.full_sanitizer.sanitize(html) end # Strips all link tags from +html+ leaving just the link text. @@ -110,6 +113,9 @@ module ActionView # # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') # # => Blog: Visit. + # + # strip_links('<<a href="https://example.org">malformed & link</a>') + # # => <malformed & link def strip_links(html) self.class.link_sanitizer.sanitize(html) end diff --git a/actionview/package.json b/actionview/package.json index ec3306c299..a1da13315e 100644 --- a/actionview/package.json +++ b/actionview/package.json @@ -1,6 +1,6 @@ { "name": "rails-ujs", - "version": "0.0.1", + "version": "5.1.0-beta1", "description": "Ruby on Rails unobtrusive scripting adapter", "main": "lib/assets/compiled/rails-ujs.js", "files": [ @@ -12,7 +12,7 @@ "scripts": { "build": "bundle exec blade build", "test": "echo \"See the README: https://github.com/rails/rails-ujs#how-to-run-tests\" && exit 1", - "lint": "coffeelint src && eslint test/public/test", + "lint": "coffeelint app/assets/javascripts && eslint test/public/test" }, "repository": { "type": "git", diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index c8963fee9c..11ed55456f 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -10,6 +10,7 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>") assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>") + assert_equal "<malformed & link", strip_links('<<a href="https://example.org">malformed & link</a>') end def test_sanitize_form @@ -26,6 +27,7 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal("Dont touch me", strip_tags("Dont touch me")) assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>")) assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.") + assert_equal("Jekyll & Hyde", strip_tags("Jekyll & Hyde")) assert_equal "", strip_tags("<script>") end diff --git a/actionview/test/ujs/config.ru b/actionview/test/ujs/config.ru index 414c2063c3..48b7a4b53a 100644 --- a/actionview/test/ujs/config.ru +++ b/actionview/test/ujs/config.ru @@ -1,3 +1,4 @@ $LOAD_PATH.unshift File.expand_path("..", __FILE__) require "server" + run UJS::Server diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb index 25f70baf5f..7deb208af0 100644 --- a/actionview/test/ujs/server.rb +++ b/actionview/test/ujs/server.rb @@ -1,11 +1,10 @@ +require "rack" require "rails" require "action_controller/railtie" require "action_view/railtie" require "blade" require "json" -JQUERY_VERSIONS = %w[ 1.8.0 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.10.0 1.10.1 1.10.2 1.11.0 2.0.0 2.1.0].freeze - module UJS class Server < Rails::Application routes.append do @@ -18,7 +17,7 @@ module UJS config.cache_classes = false config.eager_load = false config.secret_key_base = "59d7a4dbd349fa3838d79e330e39690fc22b931e7dc17d9162f03d633d526fbb92dfdb2dc9804c8be3e199631b9c1fbe43fc3e4fc75730b515851849c728d5c7" - config.paths["app/views"].unshift("#{Rails.root / "views"}") + config.paths["app/views"].unshift("#{Rails.root}/views") config.public_file_server.enabled = true config.logger = Logger.new(STDOUT) config.log_level = :error @@ -26,32 +25,6 @@ module UJS end module TestsHelper - def jquery_link(version) - if params[:version] == version - "[#{version}]" - else - "<a href='/?version=#{version}&cdn=#{params[:cdn]}'>#{version}</a>".html_safe - end - end - - def cdn_link(cdn) - if params[:cdn] == cdn - "[#{cdn}]" - else - "<a href='/?version=#{params[:version]}&cdn=#{cdn}'>#{cdn}</a>".html_safe - end - end - - def jquery_src - if params[:version] == "edge" - "/vendor/jquery.js" - elsif params[:cdn] && params[:cdn] == "googleapis" - "https://ajax.googleapis.com/ajax/libs/jquery/#{params[:version]}/jquery.min.js" - else - "http://code.jquery.com/jquery-#{params[:version]}.js" - end - end - def test_to(*names) names = ["/vendor/qunit.js", "settings"] + names names.map { |name| script_tag name }.join("\n").html_safe @@ -61,10 +34,6 @@ module TestsHelper src = "/test/#{src}.js" unless src.index("/") %(<script src="#{src}" type="text/javascript"></script>).html_safe end - - def jquery_versions - JQUERY_VERSIONS - end end class TestsController < ActionController::Base @@ -72,8 +41,6 @@ class TestsController < ActionController::Base layout "application" def index - params[:version] ||= ENV["JQUERY_VERSION"] || "1.11.0" - params[:cdn] ||= "jquery" render :index end diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb index e09b213b72..a69cd2d739 100644 --- a/actionview/test/ujs/views/layouts/application.html.erb +++ b/actionview/test/ujs/views/layouts/application.html.erb @@ -3,30 +3,15 @@ <head> <title><%= @title %></title> <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" /> - <style> - #jquery-cdn, #jquery-version { - padding: 0 2em .8em 0; - text-align: right; - font-family: sans-serif; - line-height: 1; - color: #8699A4; - background-color: #0d3349; - } - #jquery-cdn a, #jquery-version a { - color: white; - text-decoration: underline; - } - </style> - - <%= script_tag jquery_src %> + <%= script_tag "http://code.jquery.com/jquery-2.2.0.js" %> <script> // This is for test in override.js. // Must go before rails-ujs. - $(document).bind('rails:attachBindings', function() { - $.rails.linkClickSelector += ', a[data-custom-remote-link]'; + document.addEventListener('rails:attachBindings', function() { + window.Rails.linkClickSelector += ', a[data-custom-remote-link]'; // Hijacks link click before ujs binds any handlers // This is only used for ctrl-clicking test on remote links - $.rails.delegate(document, '#qunit-fixture a', 'click', function(e) { + window.Rails.delegate(document, '#qunit-fixture a', 'click', function(e) { e.preventDefault(); }); }); diff --git a/actionview/test/ujs/views/tests/index.html.erb b/actionview/test/ujs/views/tests/index.html.erb index 2ac44eeb81..8de6cd0695 100644 --- a/actionview/test/ujs/views/tests/index.html.erb +++ b/actionview/test/ujs/views/tests/index.html.erb @@ -3,20 +3,6 @@ <%= test_to 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token' %> <h1 id="qunit-header"><%= @title %></h1> -<div id="jquery-cdn"> - CDN: - <%= cdn_link 'jquery' %> • - <%= cdn_link 'googleapis' %> -</div> -<div id="jquery-version"> - jQuery version: - - <% jquery_versions.each do |v| %> - <%= ' • ' if v != jquery_versions.first %> - <%= jquery_link v %> - <% end %> - <%= (' • ' + jquery_link('edge')) if File.exist?(Rails.root + '/public/vendor/jquery.js') %> -</div> <h2 id="qunit-banner"></h2> <div id="qunit-testrunner-toolbar"></div> <h2 id="qunit-userAgent"></h2> diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 786b508e38..d561745611 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Correctly set test adapter when configure the queue adapter on a per job. Fixes #26360. diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 0d50c27938..2b608b9a65 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -8,7 +8,7 @@ module ActiveJob MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index aa97ab2e22..d7e2cd03e3 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -69,14 +69,14 @@ module ActiveJob def perform_start(event) info do job = event.payload[:job] - "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job) + "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job) end end def perform(event) info do job = event.payload[:job] - "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms" + "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms" end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 954974b2a5..b37736f859 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -89,21 +89,21 @@ class LoggingTest < ActiveSupport::TestCase def test_perform_job_logging LoggingJob.perform_later "Dummy" - assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages) assert_match(/Dummy, here is it: Dummy/, @logger.messages) - assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages) + assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages) end def test_perform_nested_jobs_logging NestedJob.perform_later assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages) assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages) assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) - assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages) - assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages) end def test_enqueue_at_job_logging diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index edaac8c7cd..1503b6a3e4 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Remove deprecated behavior that halts callbacks when the return is false. *Rafael Mendonça França* diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 4a8ee915cf..6a2ab2a8e5 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -8,7 +8,7 @@ module ActiveModel MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index 541a12c8a1..e6805c5f6b 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -21,8 +21,14 @@ module ActiveModel case value when ::Float convert_float_to_big_decimal(value) - when ::Numeric, ::String + when ::Numeric BigDecimal(value, precision || BIGDECIMAL_PRECISION) + when ::String + begin + value.to_d + rescue ArgumentError + BigDecimal(0) + end else if value.respond_to?(:to_d) value.to_d diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index 46a913258e..c3b43725cc 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -11,6 +11,14 @@ module ActiveModel assert_equal BigDecimal.new("1"), type.cast(:"1") end + def test_type_cast_decimal_from_invalid_string + type = Decimal.new + assert_nil type.cast("") + assert_equal BigDecimal.new("1"), type.cast("1ignore") + assert_equal BigDecimal.new("0"), type.cast("bad1") + assert_equal BigDecimal.new("0"), type.cast("bad") + end + def test_type_cast_decimal_from_float_with_large_precision type = Decimal.new(precision: ::Float::DIG + 2) assert_equal BigDecimal.new("123.0"), type.cast(123.0) diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb index 2e34f57f7e..8026d63ad5 100644 --- a/activemodel/test/cases/type/float_test.rb +++ b/activemodel/test/cases/type/float_test.rb @@ -9,6 +9,14 @@ module ActiveModel assert_equal 1.0, type.cast("1") end + def test_type_cast_float_from_invalid_string + type = Type::Float.new + assert_nil type.cast("") + assert_equal 1.0, type.cast("1ignore") + assert_equal 0.0, type.cast("bad1") + assert_equal 0.0, type.cast("bad") + end + def test_changing_float type = Type::Float.new diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index 2b9b03f3cf..a91144036b 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -7,6 +7,7 @@ module ActiveModel class IntegerTest < ActiveModel::TestCase test "simple values" do type = Type::Integer.new + assert_nil type.cast("") assert_equal 1, type.cast(1) assert_equal 1, type.cast("1") assert_equal 1, type.cast("1ignore") diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b27c03d935..55773f2172 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,61 @@ +* Use `max_identifier_length` for `index_name_length` in PostgreSQL adapter. + + *Ryuta Kamizono* + +* Deprecate `supports_migrations?` on connection adapters. + + *Ryuta Kamizono* + +* Fix regression of #1969 with SELECT aliases in HAVING clause. + + *Eugene Kenny* + +* Deprecate using `#quoted_id` in quoting. + + *Ryuta Kamizono* + +* Fix `wait_timeout` to configurable for mysql2 adapter. + + Fixes #26556. + + *Ryuta Kamizono* + + +## Rails 5.1.0.beta1 (February 23, 2017) ## + +* Correctly dump native timestamp types for MySQL. + + The native timestamp type in MySQL is different from datetime type. + Internal representation of the timestamp type is UNIX time, This means + that timestamp columns are affected by time zone. + + > SET time_zone = '+00:00'; + Query OK, 0 rows affected (0.00 sec) + + > INSERT INTO time_with_zone(ts,dt) VALUES (NOW(),NOW()); + Query OK, 1 row affected (0.02 sec) + + > SELECT * FROM time_with_zone; + +---------------------+---------------------+ + | ts | dt | + +---------------------+---------------------+ + | 2016-02-07 22:11:44 | 2016-02-07 22:11:44 | + +---------------------+---------------------+ + 1 row in set (0.00 sec) + + > SET time_zone = '-08:00'; + Query OK, 0 rows affected (0.00 sec) + + > SELECT * FROM time_with_zone; + +---------------------+---------------------+ + | ts | dt | + +---------------------+---------------------+ + | 2016-02-07 14:11:44 | 2016-02-07 22:11:44 | + +---------------------+---------------------+ + 1 row in set (0.00 sec) + + *Ryuta Kamizono* + * All integer-like PKs are autoincrement unless they have an explicit default. *Matthew Draper* diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 31c1e687dc..6aa414ba6b 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -50,7 +50,7 @@ module ActiveRecord super.tap do @previous_mutation_tracker = nil clear_mutation_trackers - @changed_attributes = HashWithIndifferentAccess.new + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end end @@ -70,13 +70,13 @@ module ActiveRecord def changes_applied @previous_mutation_tracker = mutation_tracker - @changed_attributes = HashWithIndifferentAccess.new + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new clear_mutation_trackers end def clear_changes_information @previous_mutation_tracker = nil - @changed_attributes = HashWithIndifferentAccess.new + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments clear_mutation_trackers end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 7f4132accf..e5a24b2aca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -7,8 +7,13 @@ module ActiveRecord # Quotes the column value to help prevent # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. def quote(value) - # records are quoted as their primary key - return value.quoted_id if value.respond_to?(:quoted_id) + value = id_value_for_database(value) if value.is_a?(Base) + + if value.respond_to?(:quoted_id) + ActiveSupport::Deprecation.warn \ + "Using #quoted_id is deprecated and will be removed in Rails 5.2." + return value.quoted_id + end _quote(value) end @@ -17,6 +22,8 @@ module ActiveRecord # SQLite does not understand dates, so this method will convert a Date # to a String. def type_cast(value, column = nil) + value = id_value_for_database(value) if value.is_a?(Base) + if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end @@ -151,6 +158,12 @@ module ActiveRecord binds.map { |attr| type_cast(attr.value_for_database) } end + def id_value_for_database(value) + if primary_key = value.class.primary_key + value.instance_variable_get(:@attributes)[primary_key].value_for_database + end + end + def types_which_need_no_typecasting [nil, Numeric, String] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 3686ad8b54..e683106527 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -334,18 +334,16 @@ module ActiveRecord # part_id int NOT NULL, # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # - def create_join_table(table_1, table_2, options = {}) + def create_join_table(table_1, table_2, column_options: {}, **options) join_table_name = find_join_table_name(table_1, table_2, options) - column_options = options.delete(:column_options) || {} - column_options.reverse_merge!(null: false) - type = column_options.delete(:type) || :integer + column_options.reverse_merge!(null: false, index: false) - t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } + t1_ref, t2_ref = [table_1, table_2].map { |t| t.to_s.singularize } create_table(join_table_name, options.merge!(id: false)) do |td| - td.send type, t1_column, column_options - td.send type, t2_column, column_options + td.references t1_ref, column_options + td.references t2_ref, column_options yield td if block_given? end end @@ -857,6 +855,7 @@ module ActiveRecord else foreign_key_options = { to_table: reference_name } end + foreign_key_options[:column] ||= "#{ref_name}_id" remove_foreign_key(table_name, **foreign_key_options) end @@ -1071,7 +1070,7 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end - elsif [:datetime, :time, :interval].include?(type) && precision ||= native[:precision] + elsif [:datetime, :timestamp, :time, :interval].include?(type) && precision ||= native[:precision] if (0..6) === precision column_type_sql << "(#{precision})" else diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b31ce0a181..ef1d9f81a9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -154,8 +154,8 @@ module ActiveRecord Arel::Visitors::ToSql.new(self) end - def valid_type?(type) - false + def valid_type?(type) # :nodoc: + !native_database_types[type].nil? end def schema_creation @@ -232,10 +232,10 @@ module ActiveRecord self.class::ADAPTER_NAME end - # Does this adapter support migrations? - def supports_migrations? - false + def supports_migrations? # :nodoc: + true end + deprecate :supports_migrations? def supports_primary_key? # :nodoc: true @@ -439,6 +439,9 @@ module ActiveRecord # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) + if ignored.size > 0 + ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.") + end reconnect! unless active? end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 14269b4570..e3b6327dd8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -46,6 +46,7 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, + timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob", limit: 65535 }, @@ -88,11 +89,6 @@ module ActiveRecord /mariadb/i.match?(full_version) end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -647,10 +643,6 @@ module ActiveRecord self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end - def valid_type?(type) - !native_database_types[type].nil? - end - def default_index_type?(index) # :nodoc: index.using == :btree || super end @@ -708,7 +700,7 @@ module ActiveRecord end def extract_precision(sql_type) - if /time/.match?(sql_type) + if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type) super || 0 else super @@ -785,11 +777,11 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) - unless options_include_default?(options) + unless options.key?(:default) options[:default] = column.default end - unless options.has_key?(:null) + unless options.key?(:null) options[:null] = column.null end @@ -869,9 +861,9 @@ module ActiveRecord variables["sql_auto_is_null"] = 0 # Increase timeout so the server doesn't disconnect us. - wait_timeout = @config[:wait_timeout] + wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout]) wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) - variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout) + variables["wait_timeout"] = wait_timeout defaults = [":default", :default].to_set diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index e8358271ab..083cd6340f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -25,6 +25,14 @@ module ActiveRecord end def add_column_options!(sql, options) + # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, + # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP + # column to contain NULL, explicitly declare it with the NULL attribute. + # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html + if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] + sql << " NULL" unless options[:null] == false || options_include_default?(options) + end + if charset = options[:charset] sql << " CHARACTER SET #{charset}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 773bbcef4e..6d88c14d50 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -75,6 +75,11 @@ module ActiveRecord super end + + private + def aliased_types(name, fallback) + fallback + end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index ad4a069d73..3e0afd9761 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -30,7 +30,10 @@ module ActiveRecord end def schema_type(column) - if column.sql_type == "tinyblob" + case column.sql_type + when /\Atimestamp\b/ + :timestamp + when "tinyblob" :blob else super @@ -38,7 +41,7 @@ module ActiveRecord end def schema_precision(column) - super unless /time/.match?(column.sql_type) && column.precision == 0 + super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0 end def schema_collation(column) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index a61d920a73..afef0da5c7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -489,7 +489,7 @@ module ActiveRecord end execute sql - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end @@ -615,10 +615,6 @@ module ActiveRecord end end - def index_name_length - 63 - end - # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c89e29ba44..bc04565434 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -215,7 +215,7 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil - @table_alias_length = nil + @max_identifier_length = nil connect add_pg_encoders @@ -281,11 +281,6 @@ module ActiveRecord NATIVE_DATABASE_TYPES end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - def set_standard_conforming_strings execute("SET standard_conforming_strings = on", "SCHEMA") end @@ -363,8 +358,9 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i + @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i end + alias index_name_length table_alias_length # Set the authorized user for this session def session_auth=(user) @@ -376,10 +372,6 @@ module ActiveRecord @use_insert_returning end - def valid_type?(type) - !native_database_types[type].nil? - end - def update_table_definition(table_name, base) #:nodoc: PostgreSQL::Table.new(table_name, base) end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 16ef195bfc..285b0ec243 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -117,11 +117,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - def requires_reloading? true end @@ -163,7 +158,7 @@ module ActiveRecord true end - def valid_type?(type) + def valid_type?(type) # :nodoc: true end @@ -420,11 +415,10 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: alter_table(table_name) do |definition| - include_default = options_include_default?(options) definition[column_name].instance_eval do self.type = type self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default + self.default = options[:default] if options.include?(:default) self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) self.scale = options[:scale] if options.include?(:scale) diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index f33456a744..174f716152 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -8,7 +8,7 @@ module ActiveRecord MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 40f6226315..263a8a7da3 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -548,12 +548,10 @@ module ActiveRecord end def call(env) - if connection.supports_migrations? - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending!(connection) - @last_check = mtime - end + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime end @app.call(env) end @@ -1098,8 +1096,6 @@ module ActiveRecord end def initialize(direction, migrations, target_version = nil) - raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? - @direction = direction @target_version = target_version @migrated_versions = nil diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 246d330b76..fabd326649 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -288,8 +288,7 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - if ActiveRecord::Base.connection.supports_migrations? && - ActiveRecord::SchemaMigration.table_exists? + if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information f.print "\n" diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 35c670f1a1..f4cdaf3948 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -232,7 +232,7 @@ module ActiveRecord query_builder = build_count_subquery(spawn, column_name, distinct) else # PostgreSQL doesn't like ORDER BY when there are no GROUP BY - relation = unscope(:order) + relation = unscope(:order).distinct!(false) column = aggregate_column(column_name) @@ -282,7 +282,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_clause.empty? + select_values += self.select_values unless having_clause.empty? select_values.concat group_columns.map { |aliaz, field| if field.respond_to?(:as) @@ -292,7 +292,7 @@ module ActiveRecord end } - relation = except(:group) + relation = except(:group).distinct!(false) relation.group_values = group_fields relation.select_values = select_values diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4548944fe6..5d24f5f5ca 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -147,7 +147,7 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit || 1) + result = limit(limit) result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key result = result.reverse_order! @@ -430,140 +430,142 @@ module ActiveRecord reflections.none?(&:collection?) end - private + def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? - def find_with_ids(*ids) - raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? - expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? + ids = ids.flatten.compact.uniq - ids = ids.flatten.compact.uniq - - case ids.size - when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" - when 1 - result = find_one(ids.first) - expects_array ? [ result ] : result - else - find_some(ids) - end - rescue ::RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + when 1 + result = find_one(ids.first) + expects_array ? [ result ] : result + else + find_some(ids) end + rescue ::RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + end - def find_one(id) - if ActiveRecord::Base === id - raise ArgumentError, <<-MSG.squish - You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id`. - MSG - end - - relation = where(primary_key => id) - record = relation.take - - raise_record_not_found_exception!(id, 0, 1) unless record - - record + def find_one(id) + if ActiveRecord::Base === id + raise ArgumentError, <<-MSG.squish + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id`. + MSG end - def find_some(ids) - return find_some_ordered(ids) unless order_values.present? + relation = where(primary_key => id) + record = relation.take - result = where(primary_key => ids).to_a + raise_record_not_found_exception!(id, 0, 1) unless record - expected_size = - if limit_value && ids.size > limit_value - limit_value - else - ids.size - end + record + end - # 11 ids with limit 3, offset 9 should give 2 results. - if offset_value && (ids.size - offset_value < expected_size) - expected_size = ids.size - offset_value - end + def find_some(ids) + return find_some_ordered(ids) unless order_values.present? - if result.size == expected_size - result + result = where(primary_key => ids).to_a + + expected_size = + if limit_value && ids.size > limit_value + limit_value else - raise_record_not_found_exception!(ids, result.size, expected_size) + ids.size end + + # 11 ids with limit 3, offset 9 should give 2 results. + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value end - def find_some_ordered(ids) - ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] + if result.size == expected_size + result + else + raise_record_not_found_exception!(ids, result.size, expected_size) + end + end - result = except(:limit, :offset).where(primary_key => ids).records + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] - if result.size == ids.size - pk_type = @klass.type_for_attribute(primary_key) + result = except(:limit, :offset).where(primary_key => ids).records - records_by_id = result.index_by(&:id) - ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } - else - raise_record_not_found_exception!(ids, result.size, ids.size) - end - end + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) - def find_take - if loaded? - records.first - else - @take ||= limit(1).records.first - end + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) end + end - def find_take_with_limit(limit) - if loaded? - records.take(limit) - else - limit(limit).to_a - end + def find_take + if loaded? + records.first + else + @take ||= limit(1).records.first end + end - def find_nth(index) - @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first + def find_take_with_limit(limit) + if loaded? + records.take(limit) + else + limit(limit).to_a end + end + + def find_nth(index) + @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first + end - def find_nth_with_limit(index, limit) - if loaded? - records[index, limit] || [] + def find_nth_with_limit(index, limit) + if loaded? + records[index, limit] || [] + else + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + self + end + if limit_value.nil? || index < limit_value relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a + else + [] end end + end - def find_nth_from_last(index) - if loaded? - records[-index] + def find_nth_from_last(index) + if loaded? + records[-index] + else + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end - - relation.to_a[-index] - # TODO: can be made more performant on large result sets by - # for instance, last(index)[-index] (which would require - # refactoring the last(n) finder method to make test suite pass), - # or by using a combination of reverse_order, limit, and offset, - # e.g., reverse_order.offset(index-1).first + self end - end - def find_last(limit) - limit ? records.last(limit) : records.last + relation.to_a[-index] + # TODO: can be made more performant on large result sets by + # for instance, last(index)[-index] (which would require + # refactoring the last(n) finder method to make test suite pass), + # or by using a combination of reverse_order, limit, and offset, + # e.g., reverse_order.offset(index-1).first end + end + + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 9ed70a9c2b..26b1d48e9e 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -41,10 +41,15 @@ module ActiveRecord @column_types = column_types end + # Returns the number of elements in the rows array. def length @rows.length end + # Calls the given block once for each element in row collection, passing + # row as parameter. + # + # Returns an +Enumerator+ if no block is given. def each if block_given? hash_rows.each { |row| yield row } @@ -53,6 +58,7 @@ module ActiveRecord end end + # Returns an array of hashes representing each row record. def to_hash hash_rows end @@ -60,11 +66,12 @@ module ActiveRecord alias :map! :map alias :collect! :map - # Returns true if there are no records. + # Returns true if there are no records, otherwise false. def empty? rows.empty? end + # Returns an array of hashes representing each row record. def to_ary hash_rows end @@ -73,11 +80,15 @@ module ActiveRecord hash_rows[idx] end + # Returns the first record from the rows collection. + # If the rows collection is empty, returns +nil+. def first return nil if @rows.empty? Hash[@columns.zip(@rows.first)] end + # Returns the last record from the rows collection. + # If the rows collection is empty, returns +nil+. def last return nil if @rows.empty? Hash[@columns.zip(@rows.last)] diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 427c0019c6..64bda1539c 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Sanitization extend ActiveSupport::Concern @@ -207,9 +206,9 @@ module ActiveRecord end end - # TODO: Deprecate this def quoted_id # :nodoc: self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) end + deprecate :quoted_id end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 15533f0151..2bbfd01698 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -85,7 +85,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.data_sources.sort - @connection.views + sorted_tables = @connection.tables.sort sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 08417aaa0f..690deee508 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -283,7 +283,7 @@ module ActiveRecord fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) - options[:if] << "transaction_include_any_action?(#{fire_on})" + options[:if].unshift("transaction_include_any_action?(#{fire_on})") end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 1f94472390..a2faf43b0d 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -42,7 +42,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase @connection.update("set @@wait_timeout=1") sleep 2 assert !@connection.active? - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end @@ -63,6 +63,18 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert @connection.active? end + def test_verify_with_args_is_deprecated + assert_deprecated do + @connection.verify!(option: true) + end + assert_deprecated do + @connection.verify!([]) + end + assert_deprecated do + @connection.verify!({}) + end + end + def test_execute_after_disconnect @connection.disconnect! @@ -85,6 +97,22 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal false, @connection.active? end + def test_wait_timeout_as_string + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(wait_timeout: "60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + + def test_wait_timeout_as_url + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge("url" => "mysql2:///?wait_timeout=60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + def test_mysql_connection_collation_is_configured assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection") assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection") diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 3cbd4ca212..c52d9e37cc 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -105,7 +105,7 @@ module ActiveRecord end def test_table_alias_length_logs_name - @connection.instance_variable_set("@table_alias_length", nil) + @connection.instance_variable_set("@max_identifier_length", nil) @connection.table_alias_length assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -177,7 +177,7 @@ module ActiveRecord assert_not_equal original_connection_pid, new_connection_pid, "umm -- looks like you didn't break the connection, because we're still " \ "successfully querying with the same connection pid." - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 141baffa5b..a1e966b915 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require "ipaddr" module ActiveRecord module ConnectionAdapters diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 9750840051..aefbb309e6 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require "bigdecimal" -require "yaml" require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase @@ -15,31 +14,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal expected, @conn.type_cast(binary) end - def test_type_cast_symbol - assert_equal "foo", @conn.type_cast(:foo) - end - - def test_type_cast_date - date = Date.today - expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date) - end - - def test_type_cast_time - time = Time.now - expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time) - end - - def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10) - assert_equal 2.2, @conn.type_cast(2.2) - end - - def test_type_cast_nil - assert_nil @conn.type_cast(nil) - end - def test_type_cast_true assert_equal "t", @conn.type_cast(true) end @@ -53,31 +27,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal bd.to_f, @conn.type_cast(bd) end - def test_type_cast_unknown_should_raise_error - obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj) } - end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end - def test_quoting_binary_strings value = "hello".encode("ascii-8bit") type = ActiveRecord::Type::String.new diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 397ac599b9..5b608d8e83 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -1,146 +1,143 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? +class ActiveRecordSchemaTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + @connection = ActiveRecord::Base.connection + ActiveRecord::SchemaMigration.drop_table + end - class ActiveRecordSchemaTest < ActiveRecord::TestCase - self.use_transactional_tests = false + teardown do + @connection.drop_table :fruits rescue nil + @connection.drop_table :nep_fruits rescue nil + @connection.drop_table :nep_schema_migrations rescue nil + @connection.drop_table :has_timestamps rescue nil + @connection.drop_table :multiple_indexes rescue nil + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = @original_verbose + end - setup do - @original_verbose = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - @connection = ActiveRecord::Base.connection - ActiveRecord::SchemaMigration.drop_table - end + def test_has_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_equal "version", ActiveRecord::SchemaMigration.primary_key - teardown do - @connection.drop_table :fruits rescue nil - @connection.drop_table :nep_fruits rescue nil - @connection.drop_table :nep_schema_migrations rescue nil - @connection.drop_table :has_timestamps rescue nil - @connection.drop_table :multiple_indexes rescue nil - ActiveRecord::SchemaMigration.delete_all rescue nil - ActiveRecord::Migration.verbose = @original_verbose + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end - def test_has_primary_key - old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type - ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_equal "version", ActiveRecord::SchemaMigration.primary_key - - ActiveRecord::SchemaMigration.create_table - assert_difference "ActiveRecord::SchemaMigration.count", 1 do - ActiveRecord::SchemaMigration.create version: 12 + def test_schema_define + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - ensure - ActiveRecord::SchemaMigration.drop_table - ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end - def test_schema_define - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end - end - - assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } - assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } - assert_equal 7, ActiveRecord::Migrator::current_version - end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } + assert_equal 7, ActiveRecord::Migrator::current_version + end - def test_schema_define_w_table_name_prefix - table_name = ActiveRecord::SchemaMigration.table_name - old_table_name_prefix = ActiveRecord::Base.table_name_prefix - ActiveRecord::Base.table_name_prefix = "nep_" - ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end + def test_schema_define_w_table_name_prefix + table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix + ActiveRecord::Base.table_name_prefix = "nep_" + ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - assert_equal 7, ActiveRecord::Migrator::current_version - ensure - ActiveRecord::Base.table_name_prefix = old_table_name_prefix - ActiveRecord::SchemaMigration.table_name = table_name end + assert_equal 7, ActiveRecord::Migrator::current_version + ensure + ActiveRecord::Base.table_name_prefix = old_table_name_prefix + ActiveRecord::SchemaMigration.table_name = table_name + end - def test_schema_raises_an_error_for_invalid_column_type - assert_raise NoMethodError do - ActiveRecord::Schema.define(version: 8) do - create_table :vegetables do |t| - t.unknown :color - end + def test_schema_raises_an_error_for_invalid_column_type + assert_raise NoMethodError do + ActiveRecord::Schema.define(version: 8) do + create_table :vegetables do |t| + t.unknown :color end end end + end - def test_schema_subclass - Class.new(ActiveRecord::Schema).define(version: 9) do - create_table :fruits - end - assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(version: 9) do + create_table :fruits end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end - def test_normalize_version - assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") - assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") - assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") - assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") - end + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end - def test_schema_load_with_multiple_indexes_for_column_of_different_names - ActiveRecord::Schema.define do - create_table :multiple_indexes do |t| - t.string "foo" - t.index ["foo"], name: "multiple_indexes_foo_1" - t.index ["foo"], name: "multiple_indexes_foo_2" - end + def test_schema_load_with_multiple_indexes_for_column_of_different_names + ActiveRecord::Schema.define do + create_table :multiple_indexes do |t| + t.string "foo" + t.index ["foo"], name: "multiple_indexes_foo_1" + t.index ["foo"], name: "multiple_indexes_foo_2" end + end - indexes = @connection.indexes("multiple_indexes") + indexes = @connection.indexes("multiple_indexes") - assert_equal 2, indexes.length - assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort - end + assert_equal 2, indexes.length + assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort + end - def test_timestamps_without_null_set_null_to_false_on_create_table - ActiveRecord::Schema.define do - create_table :has_timestamps do |t| - t.timestamps - end + def test_timestamps_without_null_set_null_to_false_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps end - - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end - def test_timestamps_without_null_set_null_to_false_on_change_table - ActiveRecord::Schema.define do - create_table :has_timestamps + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - change_table :has_timestamps do |t| - t.timestamps default: Time.now - end - end + def test_timestamps_without_null_set_null_to_false_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + change_table :has_timestamps do |t| + t.timestamps default: Time.now + end end - def test_timestamps_without_null_set_null_to_false_on_add_timestamps - ActiveRecord::Schema.define do - create_table :has_timestamps - add_timestamps :has_timestamps, default: Time.now - end + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + def test_timestamps_without_null_set_null_to_false_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now end + + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 5d1c1c4b9b..16eff15026 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,147 +1,146 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? - class EagerSingularizationTest < ActiveRecord::TestCase - class Virus < ActiveRecord::Base - belongs_to :octopus - end - - class Octopus < ActiveRecord::Base - has_one :virus - end - - class Pass < ActiveRecord::Base - belongs_to :bus - end - - class Bus < ActiveRecord::Base - has_many :passes - end - - class Mess < ActiveRecord::Base - has_and_belongs_to_many :crises - end - - class Crisis < ActiveRecord::Base - has_and_belongs_to_many :messes - has_many :analyses, dependent: :destroy - has_many :successes, through: :analyses - has_many :dresses, dependent: :destroy - has_many :compresses, through: :dresses - end - - class Analysis < ActiveRecord::Base - belongs_to :crisis - belongs_to :success - end - - class Success < ActiveRecord::Base - has_many :analyses, dependent: :destroy - has_many :crises, through: :analyses - end - - class Dress < ActiveRecord::Base - belongs_to :crisis - has_many :compresses - end - - class Compress < ActiveRecord::Base - belongs_to :dress - end - - def setup - connection.create_table :viri do |t| - t.column :octopus_id, :integer - t.column :species, :string - end - connection.create_table :octopi do |t| - t.column :species, :string - end - connection.create_table :passes do |t| - t.column :bus_id, :integer - t.column :rides, :integer - end - connection.create_table :buses do |t| - t.column :name, :string - end - connection.create_table :crises_messes, id: false do |t| - t.column :crisis_id, :integer - t.column :mess_id, :integer - end - connection.create_table :messes do |t| - t.column :name, :string - end - connection.create_table :crises do |t| - t.column :name, :string - end - connection.create_table :successes do |t| - t.column :name, :string - end - connection.create_table :analyses do |t| - t.column :crisis_id, :integer - t.column :success_id, :integer - end - connection.create_table :dresses do |t| - t.column :crisis_id, :integer - end - connection.create_table :compresses do |t| - t.column :dress_id, :integer - end - end - - teardown do - connection.drop_table :viri - connection.drop_table :octopi - connection.drop_table :passes - connection.drop_table :buses - connection.drop_table :crises_messes - connection.drop_table :messes - connection.drop_table :crises - connection.drop_table :successes - connection.drop_table :analyses - connection.drop_table :dresses - connection.drop_table :compresses - end +class EagerSingularizationTest < ActiveRecord::TestCase + class Virus < ActiveRecord::Base + belongs_to :octopus + end - def connection - ActiveRecord::Base.connection + class Octopus < ActiveRecord::Base + has_one :virus + end + + class Pass < ActiveRecord::Base + belongs_to :bus + end + + class Bus < ActiveRecord::Base + has_many :passes + end + + class Mess < ActiveRecord::Base + has_and_belongs_to_many :crises + end + + class Crisis < ActiveRecord::Base + has_and_belongs_to_many :messes + has_many :analyses, dependent: :destroy + has_many :successes, through: :analyses + has_many :dresses, dependent: :destroy + has_many :compresses, through: :dresses + end + + class Analysis < ActiveRecord::Base + belongs_to :crisis + belongs_to :success + end + + class Success < ActiveRecord::Base + has_many :analyses, dependent: :destroy + has_many :crises, through: :analyses + end + + class Dress < ActiveRecord::Base + belongs_to :crisis + has_many :compresses + end + + class Compress < ActiveRecord::Base + belongs_to :dress + end + + def setup + connection.create_table :viri do |t| + t.column :octopus_id, :integer + t.column :species, :string end + connection.create_table :octopi do |t| + t.column :species, :string + end + connection.create_table :passes do |t| + t.column :bus_id, :integer + t.column :rides, :integer + end + connection.create_table :buses do |t| + t.column :name, :string + end + connection.create_table :crises_messes, id: false do |t| + t.column :crisis_id, :integer + t.column :mess_id, :integer + end + connection.create_table :messes do |t| + t.column :name, :string + end + connection.create_table :crises do |t| + t.column :name, :string + end + connection.create_table :successes do |t| + t.column :name, :string + end + connection.create_table :analyses do |t| + t.column :crisis_id, :integer + t.column :success_id, :integer + end + connection.create_table :dresses do |t| + t.column :crisis_id, :integer + end + connection.create_table :compresses do |t| + t.column :dress_id, :integer + end + end - def test_eager_no_extra_singularization_belongs_to - assert_nothing_raised do - Virus.all.merge!(includes: :octopus).to_a - end + teardown do + connection.drop_table :viri + connection.drop_table :octopi + connection.drop_table :passes + connection.drop_table :buses + connection.drop_table :crises_messes + connection.drop_table :messes + connection.drop_table :crises + connection.drop_table :successes + connection.drop_table :analyses + connection.drop_table :dresses + connection.drop_table :compresses + end + + def test_eager_no_extra_singularization_belongs_to + assert_nothing_raised do + Virus.all.merge!(includes: :octopus).to_a end + end - def test_eager_no_extra_singularization_has_one - assert_nothing_raised do - Octopus.all.merge!(includes: :virus).to_a - end + def test_eager_no_extra_singularization_has_one + assert_nothing_raised do + Octopus.all.merge!(includes: :virus).to_a end + end - def test_eager_no_extra_singularization_has_many - assert_nothing_raised do - Bus.all.merge!(includes: :passes).to_a - end + def test_eager_no_extra_singularization_has_many + assert_nothing_raised do + Bus.all.merge!(includes: :passes).to_a end + end - def test_eager_no_extra_singularization_has_and_belongs_to_many - assert_nothing_raised do - Crisis.all.merge!(includes: :messes).to_a - Mess.all.merge!(includes: :crises).to_a - end + def test_eager_no_extra_singularization_has_and_belongs_to_many + assert_nothing_raised do + Crisis.all.merge!(includes: :messes).to_a + Mess.all.merge!(includes: :crises).to_a end + end - def test_eager_no_extra_singularization_has_many_through_belongs_to - assert_nothing_raised do - Crisis.all.merge!(includes: :successes).to_a - end + def test_eager_no_extra_singularization_has_many_through_belongs_to + assert_nothing_raised do + Crisis.all.merge!(includes: :successes).to_a end + end - def test_eager_no_extra_singularization_has_many_through_has_many - assert_nothing_raised do - Crisis.all.merge!(includes: :compresses).to_a - end + def test_eager_no_extra_singularization_has_many_through_has_many + assert_nothing_raised do + Crisis.all.merge!(includes: :compresses).to_a end end + + private + def connection + ActiveRecord::Base.connection + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 1813534b62..f018156cfc 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -227,6 +227,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_match "credit_limit, firm_name", e.message end + def test_apply_distinct_in_count + queries = assert_sql do + Account.distinct.count + Account.group(:firm_id).distinct.count + end + + queries.each do |query| + # `table_alias_length` in `column_alias_for` would execute + # "SHOW max_identifier_length" statement in PostgreSQL adapter. + next if query == "SHOW max_identifier_length" + assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query + end + end + def test_should_group_by_summed_field_having_condition c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit) assert_nil c[1] @@ -235,7 +249,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition_from_select - c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit) + skip if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) + c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit) assert_nil c[1] assert_equal 60, c[2] assert_equal 53, c[9] diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index afd0ac2dd4..7e88c9cf7a 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -307,14 +307,17 @@ module ActiveRecord end end - def test_automatic_reconnect= + def test_automatic_reconnect_restores_after_disconnect pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec assert pool.automatic_reconnect assert pool.connection pool.disconnect! assert pool.connection + end + def test_automatic_reconnect_can_be_disabled + pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec pool.disconnect! pool.automatic_reconnect = false diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 6532efcf22..a6297673c9 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -100,11 +100,21 @@ if current_adapter?(:Mysql2Adapter) include SchemaDumpingHelper if ActiveRecord::Base.connection.version >= "5.6.0" - test "schema dump includes default expression" do + test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output end end + + test "schema dump timestamp includes default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + + test "schema dump timestamp without default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + end end class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index a43c06cd6e..c13a962e3e 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -566,19 +566,17 @@ class DirtyTest < ActiveRecord::TestCase travel_back end - if ActiveRecord::Base.connection.supports_migrations? - class Testings < ActiveRecord::Base; end - def test_field_named_field - ActiveRecord::Base.connection.create_table :testings do |t| - t.string :field - end - assert_nothing_raised do - Testings.new.attributes - end - ensure - ActiveRecord::Base.connection.drop_table :testings rescue nil - ActiveRecord::Base.clear_cache! + class Testings < ActiveRecord::Base; end + def test_field_named_field + ActiveRecord::Base.connection.create_table :testings do |t| + t.string :field end + assert_nothing_raised do + Testings.new.attributes + end + ensure + ActiveRecord::Base.connection.drop_table :testings rescue nil + ActiveRecord::Base.clear_cache! end def test_datetime_attribute_can_be_updated_with_fractional_seconds diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index deec669935..89d8a8bdca 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -497,7 +497,7 @@ class FinderTest < ActiveRecord::TestCase assert_nil Topic.offset(5).second_to_last #test with limit - # assert_nil Topic.limit(1).second # TODO: currently failing + assert_nil Topic.limit(1).second assert_nil Topic.limit(1).second_to_last end @@ -526,9 +526,9 @@ class FinderTest < ActiveRecord::TestCase assert_nil Topic.offset(5).third_to_last # test with limit - # assert_nil Topic.limit(1).third # TODO: currently failing + assert_nil Topic.limit(1).third assert_nil Topic.limit(1).third_to_last - # assert_nil Topic.limit(2).third # TODO: currently failing + assert_nil Topic.limit(2).third assert_nil Topic.limit(2).third_to_last end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index afe761cb55..51133e9495 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -104,64 +104,62 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(second_row["author_email_address"]) end - if ActiveRecord::Base.connection.supports_migrations? - def test_inserts_with_pre_and_suffix - # Reset cache to make finds on the new table work - ActiveRecord::FixtureSet.reset_cache - - ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| - t.column :title, :string - t.column :author_name, :string - t.column :author_email_address, :string - t.column :written_on, :datetime - t.column :bonus_time, :time - t.column :last_read, :date - t.column :content, :string - t.column :approved, :boolean, default: true - t.column :replies_count, :integer, default: 0 - t.column :parent_id, :integer - t.column :type, :string, limit: 50 - end + def test_inserts_with_pre_and_suffix + # Reset cache to make finds on the new table work + ActiveRecord::FixtureSet.reset_cache - # Store existing prefix/suffix - old_prefix = ActiveRecord::Base.table_name_prefix - old_suffix = ActiveRecord::Base.table_name_suffix + ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| + t.column :title, :string + t.column :author_name, :string + t.column :author_email_address, :string + t.column :written_on, :datetime + t.column :bonus_time, :time + t.column :last_read, :date + t.column :content, :string + t.column :approved, :boolean, default: true + t.column :replies_count, :integer, default: 0 + t.column :parent_id, :integer + t.column :type, :string, limit: 50 + end - # Set a prefix/suffix we can test against - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" + # Store existing prefix/suffix + old_prefix = ActiveRecord::Base.table_name_prefix + old_suffix = ActiveRecord::Base.table_name_suffix - other_topic_klass = Class.new(ActiveRecord::Base) do - def self.name - "OtherTopic" - end + # Set a prefix/suffix we can test against + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + + other_topic_klass = Class.new(ActiveRecord::Base) do + def self.name + "OtherTopic" end + end - topics = [create_fixtures("other_topics")].flatten.first + topics = [create_fixtures("other_topics")].flatten.first - # This checks for a caching problem which causes a bug in the fixtures - # class-level configuration helper. - assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" + # This checks for a caching problem which causes a bug in the fixtures + # class-level configuration helper. + assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" - first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") - assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" - assert_equal("The First Topic", first_row["title"]) + first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") + assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" + assert_equal("The First Topic", first_row["title"]) - second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") - assert_nil(second_row["author_email_address"]) + second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") + assert_nil(second_row["author_email_address"]) - assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym - # This assertion should preferably be the last in the list, because calling - # other_topic_klass.table_name sets a class-level instance variable - assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym + assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym + # This assertion should preferably be the last in the list, because calling + # other_topic_klass.table_name sets a class-level instance variable + assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym - ensure - # Restore prefix/suffix to its previous values - ActiveRecord::Base.table_name_prefix = old_prefix - ActiveRecord::Base.table_name_suffix = old_suffix + ensure + # Restore prefix/suffix to its previous values + ActiveRecord::Base.table_name_prefix = old_prefix + ActiveRecord::Base.table_name_suffix = old_suffix - ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil - end + ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil end def test_insert_with_datetime diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 48cfe89882..1d305fa11f 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -269,6 +269,8 @@ module ActiveRecord if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + elsif current_adapter?(:Mysql2Adapter) + assert_equal "timestamp", klass.columns_hash["foo"].sql_type else assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 55c06da411..2329888345 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -225,6 +225,16 @@ module ActiveRecord assert_nil TestModel.new.contributor end + def test_change_column_to_drop_default_with_null_false + add_column "test_models", "contributor", :boolean, default: true, null: false + assert TestModel.new.contributor? + + change_column "test_models", "contributor", :boolean, default: nil, null: false + TestModel.reset_column_information + assert_not TestModel.new.contributor? + assert_nil TestModel.new.contributor + end + def test_change_column_with_new_default add_column "test_models", "administrator", :boolean, default: true assert TestModel.new.administrator? diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 26b1bb4419..c4896f3d6e 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,7 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.table_exists?(table_name) + connection.drop_table table_name, if_exists: true end end @@ -78,6 +78,17 @@ module ActiveRecord assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) end + def test_create_join_table_respects_reference_key_type + connection.create_join_table :artists, :musics do |t| + t.references :video + end + + artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name) + + assert_equal video_id.sql_type, artist_id.sql_type + assert_equal video_id.sql_type, music_id.sql_type + end + def test_drop_join_table connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 61f5a061b0..6970fdcc87 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -21,8 +21,6 @@ module ActiveRecord end def test_errors_if_pending - @connection.expect :supports_migrations?, true - ActiveRecord::Migrator.stub :needs_migration?, true do assert_raise ActiveRecord::PendingMigrationError do @pending.call(nil) @@ -31,22 +29,12 @@ module ActiveRecord end def test_checks_if_supported - @connection.expect :supports_migrations?, true @app.expect :call, nil, [:foo] ActiveRecord::Migrator.stub :needs_migration?, false do @pending.call(:foo) end end - - def test_doesnt_check_if_unsupported - @connection.expect :supports_migrations?, false - @app.expect :call, nil, [:foo] - - ActiveRecord::Migrator.stub :needs_migration?, true do - @pending.call(:foo) - end - end end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 9418995ea0..f1ddac1ee2 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -203,6 +203,22 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal([["testings", "testing_parents", "parent1_id"], ["testings", "testing_parents", "parent2_id"]], fk_definitions) end + + test "multiple foreign keys can be removed to the selected one" do + @connection.create_table :testings do |t| + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :parent1, foreign_key: { to_table: :testing_parents } + end + + fks = @connection.foreign_keys("testings").sort_by(&:column) + + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "parent2_id"]], fk_definitions) + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index de16ecf442..fabb1662a3 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1142,4 +1142,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_deprecate_migration_keys assert_deprecated { ActiveRecord::Base.connection.migration_keys } end + + def test_deprecate_supports_migrations + assert_deprecated { ActiveRecord::Base.connection.supports_migrations? } + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 03c8644229..12386635f6 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -291,6 +291,14 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema end + + if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported? + test "schema typed primary key column" do + @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true) + schema = dump_table_schema("scheduled_logs") + assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema + end + end end class CompositePrimaryKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 5ff5e3c735..f260d043e4 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -82,7 +82,7 @@ module ActiveRecord end def test_quote_with_quoted_id - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) + assert_deprecated { assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) } end def test_quote_nil @@ -150,6 +150,62 @@ module ActiveRecord end end + class TypeCastingTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_symbol + assert_equal "foo", @conn.type_cast(:foo) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) + end + + def test_type_cast_nil + assert_nil @conn.type_cast(nil) + end + + def test_type_cast_unknown_should_raise_error + obj = Class.new.new + assert_raise(TypeError) { @conn.type_cast(obj) } + end + + def test_type_cast_object_which_responds_to_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 + end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + }.new + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + end + class QuoteBooleanTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection @@ -165,5 +221,32 @@ module ActiveRecord assert_predicate @connection.type_cast(false), :frozen? end end + + if subsecond_precision_supported? + class QuoteARBaseTest < ActiveRecord::TestCase + class DatetimePrimaryKey < ActiveRecord::Base + end + + def setup + @time = ::Time.utc(2017, 2, 14, 12, 34, 56, 789999) + @connection = ActiveRecord::Base.connection + @connection.create_table :datetime_primary_keys, id: :datetime, precision: 3, force: true + end + + def teardown + @connection.drop_table :datetime_primary_keys, if_exists: true + end + + def test_quote_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "'2017-02-14 12:34:56.789000'", @connection.quote(value) + end + + def test_type_cast_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "2017-02-14 12:34:56.789000", @connection.type_cast(value) + end + end + end end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 23bcb0af1e..72f09186e2 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -152,11 +152,15 @@ class SanitizeTest < ActiveRecord::TestCase end def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal "1", bind("?", o) + o = Class.new { + def quoted_id + 1 + end + }.new + assert_deprecated { assert_equal "1", bind("?", o) } os = [o] * 3 - assert_equal "1,1,1", bind("?", os) + assert_deprecated { assert_equal "1,1,1", bind("?", os) } end def test_named_bind_with_postgresql_type_casts diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 995ff4dfc5..d261fd5321 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -161,7 +161,7 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_first_and_last_should_allow_integers_for_limit - assert_equal Topic.base.first(2), Topic.base.to_a.first(2) + assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2) assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2) end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 391bbe8877..eaa4dd09a9 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -551,3 +551,43 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:rollback], @topic.history end end + +class CallbacksOnActionAndConditionTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class TopicWithCallbacksOnActionAndCondition < ActiveRecord::Base + self.table_name = :topics + + after_commit(on: [:create, :update], if: :run_callback?) { |record| record.history << :create_or_update } + + def clear_history + @history = [] + end + + def history + @history ||= [] + end + + def run_callback? + self.history << :run_callback? + true + end + + attr_accessor :save_before_commit_history, :update_title + end + + def test_callback_on_action_with_condition + topic = TopicWithCallbacksOnActionAndCondition.new + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.approved = true + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.destroy + assert_equal [], topic.history + end +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 9a203a7293..90a314c83c 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -6,6 +6,11 @@ ActiveRecord::Schema.define do end end + create_table :timestamp_defaults, force: true do |t| + t.timestamp :nullable_timestamp + t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index bc5af36a28..1a609e13c3 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -10,7 +10,10 @@ module ARTest end def self.connection_config - config["connections"][connection_name] + config.fetch("connections").fetch(connection_name) do + puts "Connection #{connection_name.inspect} not found. Available connections: #{config['connections'].keys.join(', ')}" + exit 1 + end end def self.connect diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 7962427032..0614cdaabd 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,8 +1,42 @@ +* Add `ActiveSupport::Duration#before` and `#after` as aliases for `#until` and `#since` + + These read more like English and require less mental gymnastics to read and write. + + Before: + + 2.weeks.since(customer_start_date) + 5.days.until(today) + + After: + + 2.weeks.after(customer_start_date) + 5.days.before(today) + + *Nick Johnstone* + +* Soft-deprecated the top-level `HashWithIndifferentAccess` constant. + `ActiveSupport::HashWithIndifferentAccess` should be used instead. + + *Robin Dupret* (#28157) + +* In Core Extensions, make `MarshalWithAutoloading#load` pass through the second, optional + argument for `Marshal#load( source [, proc] )`. This way we don't have to do + `Marshal.method(:load).super_method.call(source, proc)` just to be able to pass a proc. + + *Jeff Latz* + +* `ActiveSupport::Gzip.decompress` now checks checksum and length in footer. + + *Dylan Thacker-Smith* + + +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Cache `ActiveSupport::TimeWithZone#to_datetime` before freezing. *Adam Rice* -* Deprecate `.halt_callback_chains_on_return_false`. +* Deprecate `ActiveSupport.halt_callback_chains_on_return_false`. *Rafael Mendonça França* @@ -65,7 +99,7 @@ duration's numeric value isn't used in calculations, only parts are used. Methods on `Numeric` like `2.days` now use these predefined durations - to avoid duplicating of duration constants through the codebase and + to avoid duplication of duration constants through the codebase and eliminate creation of intermediate durations. *Andrey Novikov, Andrew White* diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index edfc8296fe..bba2b3be2e 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,7 +1,7 @@ module ActiveSupport module MarshalWithAutoloading # :nodoc: - def load(source) - super(source) + def load(source, proc = nil) + super(source, proc) rescue ArgumentError, NameError => exc if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|) # try loading the class/module diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 003f6203ef..70cf78519d 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -180,6 +180,7 @@ module ActiveSupport sum(1, time) end alias :from_now :since + alias :after :since # Calculates a new Time or Date that is as far in the past # as this Duration represents. @@ -187,6 +188,7 @@ module ActiveSupport sum(-1, time) end alias :until :ago + alias :before :ago def inspect #:nodoc: parts. diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index 51d53e2f8d..e5d458b3ab 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -4,7 +4,7 @@ require "active_support/core_ext/hash/transform_values" module ActiveSupport class Duration # Serializes duration to string according to ISO 8601 Duration format. - class ISO8601Serializer + class ISO8601Serializer # :nodoc: def initialize(duration, precision: nil) @duration = duration @precision = precision diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 74f2d8dd4b..a641b96c57 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -8,7 +8,7 @@ module ActiveSupport MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 84eef6a623..95a86889ec 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -21,7 +21,7 @@ module ActiveSupport # Decompresses a gzipped string. def self.decompress(source) - Zlib::GzipReader.new(StringIO.new(source)).read + Zlib::GzipReader.wrap(StringIO.new(source), &:read) end # Compresses a string using gzip. diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 79e7feaf47..1927cddf34 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -270,7 +270,7 @@ module ActiveSupport end def compact - dup.compact! + dup.tap(&:compact!) end # Convert to a regular hash with string keys. @@ -316,4 +316,6 @@ module ActiveSupport end end +# :stopdoc: + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 6a275d1d5b..6facb04f1f 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -179,6 +179,19 @@ class DurationTest < ActiveSupport::TestCase Time.zone = nil end + def test_before_and_afer + t = Time.local(2000) + assert_equal t + 1, 1.second.after(t) + assert_equal t - 1, 1.second.before(t) + end + + def test_before_and_after_without_argument + Time.stub(:now, Time.local(2000)) do + assert_equal Time.now - 1.second, 1.second.before + assert_equal Time.now + 1.second, 1.second.after + end + end + def test_adding_hours_across_dst_boundary with_env_tz "CET" do assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 24.hours, Time.local(2009, 3, 30, 1, 0, 0) diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 05813ad388..525ea08abd 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -8,6 +8,8 @@ require "active_support/core_ext/object/deep_dup" require "active_support/inflections" class HashExtTest < ActiveSupport::TestCase + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess + class IndifferentHash < ActiveSupport::HashWithIndifferentAccess end @@ -597,6 +599,16 @@ class HashExtTest < ActiveSupport::TestCase assert_equal(@strings, compacted_hash) assert_equal(hash_contain_nil_value, hash) assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash + + empty_hash = ActiveSupport::HashWithIndifferentAccess.new + compacted_hash = empty_hash.compact + + assert_equal compacted_hash, empty_hash + + non_empty_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: :bar) + compacted_hash = non_empty_hash.compact + + assert_equal compacted_hash, non_empty_hash end def test_indifferent_to_hash @@ -1078,6 +1090,30 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, hash[:a] assert_equal 3, hash[:b] end + + def test_inheriting_from_top_level_hash_with_indifferent_access_preserves_ancestors_chain + klass = Class.new(::HashWithIndifferentAccess) + assert_equal ActiveSupport::HashWithIndifferentAccess, klass.ancestors[1] + end + + def test_inheriting_from_hash_with_indifferent_access_properly_dumps_ivars + klass = Class.new(::HashWithIndifferentAccess) do + def initialize(*) + @foo = "bar" + super + end + end + + yaml_output = klass.new.to_yaml + + # `hash-with-ivars` was introduced in 2.0.9 (https://git.io/vyUQW) + if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("2.0.9") + assert_includes yaml_output, "hash-with-ivars" + assert_includes yaml_output, "@foo: bar" + else + assert_includes yaml_output, "hash" + end + end end class IWriteMyOwnXML @@ -1123,6 +1159,8 @@ class HashExtToParamTests < ActiveSupport::TestCase end class HashToXmlTest < ActiveSupport::TestCase + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess + def setup @xml_options = { root: :person, skip_instruct: true, indent: 0 } end diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index a899f98705..cabeed2fae 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -19,6 +19,19 @@ class MarshalTest < ActiveSupport::TestCase end end + test "that Marshal#load still works when passed a proc" do + example_string = "test" + + example_proc = Proc.new do |o| + if o.is_a?(String) + o.capitalize! + end + end + + dumped = Marshal.dump(example_string) + assert_equal Marshal.load(dumped, example_proc), "Test" + end + test "that a missing class is autoloaded from string" do dumped = nil with_autoloading_fixtures do diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb index f51d3cdf65..33e0cd2a04 100644 --- a/activesupport/test/gzip_test.rb +++ b/activesupport/test/gzip_test.rb @@ -30,4 +30,14 @@ class GzipTest < ActiveSupport::TestCase assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize) end + + def test_decompress_checks_crc + compressed = ActiveSupport::Gzip.compress("Hello World") + first_crc_byte_index = compressed.bytesize - 8 + compressed.setbyte(first_crc_byte_index, compressed.getbyte(first_crc_byte_index) ^ 0xff) + + assert_raises(Zlib::GzipFile::CRCError) do + ActiveSupport::Gzip.decompress(compressed) + end + end end diff --git a/ci/phantomjs.js b/ci/phantomjs.js new file mode 100644 index 0000000000..7a33fb14a3 --- /dev/null +++ b/ci/phantomjs.js @@ -0,0 +1,149 @@ +/* + * PhantomJS Runner QUnit Plugin 1.2.0 + * + * PhantomJS binaries: http://phantomjs.org/download.html + * Requires PhantomJS 1.6+ (1.7+ recommended) + * + * Run with: + * phantomjs runner.js [url-of-your-qunit-testsuite] + * + * e.g. + * phantomjs runner.js http://localhost/qunit/test/index.html + */ + +/*global phantom:false, require:false, console:false, window:false, QUnit:false */ + +(function() { + 'use strict'; + + var url, page, timeout, + args = require('system').args; + + // arg[0]: scriptName, args[1...]: arguments + if (args.length < 2 || args.length > 3) { + console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); + phantom.exit(1); + } + + url = args[1]; + page = require('webpage').create(); + if (args[2] !== undefined) { + timeout = parseInt(args[2], 10); + } + + // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onInitialized = function() { + page.evaluate(addLogging); + }; + + page.onCallback = function(message) { + var result, + failed; + + if (message) { + if (message.name === 'QUnit.done') { + result = message.data; + failed = !result || !result.total || result.failed; + + if (!result.total) { + console.error('No tests were executed. Are you loading tests asynchronously?'); + } + + phantom.exit(failed ? 1 : 0); + } + } + }; + + page.open(url, function(status) { + if (status !== 'success') { + console.error('Unable to access network: ' + status); + phantom.exit(1); + } else { + // Cannot do this verification with the 'DOMContentLoaded' handler because it + // will be too late to attach it if a page does not have any script tags. + var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); + if (qunitMissing) { + console.error('The `QUnit` object is not present on this page.'); + phantom.exit(1); + } + + // Set a timeout on the test running, otherwise tests with async problems will hang forever + if (typeof timeout === 'number') { + setTimeout(function() { + console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); + phantom.exit(1); + }, timeout * 1000); + } + + // Do nothing... the callback mechanism will handle everything! + } + }); + + function addLogging() { + window.document.addEventListener('DOMContentLoaded', function() { + var currentTestAssertions = []; + + QUnit.log(function(details) { + var response; + + // Ignore passing assertions + if (details.result) { + return; + } + + response = details.message || ''; + + if (typeof details.expected !== 'undefined') { + if (response) { + response += ', '; + } + + response += 'expected: ' + details.expected + ', but was: ' + details.actual; + } + + if (details.source) { + response += "\n" + details.source; + } + + currentTestAssertions.push('Failed assertion: ' + response); + }); + + QUnit.testDone(function(result) { + var i, + len, + name = ''; + + if (result.module) { + name += result.module + ': '; + } + name += result.name; + + if (result.failed) { + console.log('\n' + 'Test failed: ' + name); + + for (i = 0, len = currentTestAssertions.length; i < len; i++) { + console.log(' ' + currentTestAssertions[i]); + } + } + + currentTestAssertions.length = 0; + }); + + QUnit.done(function(result) { + console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); + + if (typeof window.callPhantom === 'function') { + window.callPhantom({ + 'name': 'QUnit.done', + 'data': result + }); + } + }); + }, false); + } +})(); + diff --git a/ci/travis.rb b/ci/travis.rb index f59ce5406a..eb2890ca70 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -36,8 +36,10 @@ class Build def run!(options = {}) self.options.update(options) + Dir.chdir(dir) do announce(heading) + if guides? run_bug_report_templates else @@ -69,7 +71,7 @@ class Build end tasks else - ["test", ("isolated" if isolated?), ("integration" if integration?)].compact.join(":") + ["test", ("isolated" if isolated?), ("integration" if integration?), ("ujs" if ujs?)].compact.join(":") end end @@ -92,6 +94,10 @@ class Build gem == "guides" end + def ujs? + component.split(":").last == "ujs" + end + def isolated? options[:isolated] end @@ -151,6 +157,7 @@ ENV["GEM"].split(",").each do |gem| next if gem == "ac:integration" && isolated next if gem == "aj:integration" && isolated next if gem == "guides" && isolated + next if gem == "av:ujs" && isolated build = Build.new(gem, isolated: isolated) results[build.key] = build.run! diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 2730d2dfea..3a602efb3d 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,2 +1,6 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + +* No changes. + Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 7644f6fe4a..486c7243ad 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "action_controller/railtie" diff --git a/guides/source/i18n.md b/guides/source/i18n.md index ed8cf8a344..6c8706bc13 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -707,6 +707,7 @@ The `:count` interpolation variable has a special role in that it both is interp ```ruby I18n.backend.store_translations :en, inbox: { + zero: 'no messages', # optional one: 'one message', other: '%{count} messages' } @@ -715,15 +716,20 @@ I18n.translate :inbox, count: 2 I18n.translate :inbox, count: 1 # => 'one message' + +I18n.translate :inbox, count: 0 +# => 'no messages' ``` The algorithm for pluralizations in `:en` is as simple as: ```ruby -entry[count == 1 ? 0 : 1] +lookup_key = :zero if count == 0 && entry.has_key?(:zero) +lookup_key ||= count == 1 ? :one : :other +entry[lookup_key] ``` -I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero). +The translation denoted as `:one` is regarded as singular, and the `:other` is used as plural. If the count is zero, and a `:zero` entry is present, then it will be used instead of `:other`. If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised. diff --git a/guides/source/testing.md b/guides/source/testing.md index 652030a733..4231500729 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1435,6 +1435,10 @@ variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. The helper `read_fixture` is used to read in the content from this file. +NOTE: `email.body.to_s` is present when there's only one (HTML or text) part present. +If the mailer provides both, you can test your fixture against specific parts +with `email.text_part.body.to_s` or `email.html_part.body.to_s`. + Here's the content of the `invite` fixture: ``` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 8ba00a2b10..6005298127 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -65,6 +65,25 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] Don't forget to review the difference, to see if there were any unexpected changes. +Upgrading from Rails 5.0 to Rails 5.1 +------------------------------------- + +For more information on changes made to Rails 5.1 please see the [release notes](5_1_release_notes.html). + +### Top-level `HashWithIndifferentAccess` is soft-deprecated + +If your application uses the the top-level `HashWithIndifferentAccess` class, you +should slowly move your code to use the `ActiveSupport::HashWithIndifferentAccess` +one. + +It is only soft-deprecated, which means that your code will not break at the +moment and no deprecation warning will be displayed but this constant will be +removed in the future. + +Also, if you have pretty old YAML documents containg dumps of such objects, +you may need to load and dump them again to make sure that they reference +the right constant and that loading them won't break in the future. + Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb index c0a32c6b91..4671e040ca 100644 --- a/guides/w3c_validator.rb +++ b/guides/w3c_validator.rb @@ -32,7 +32,8 @@ include W3CValidators module RailsGuides class Validator def validate - validator = MarkupValidator.new + # https://github.com/w3c-validators/w3c_validators/issues/25 + validator = NuValidator.new STDOUT.sync = true errors_on_guides = {} @@ -44,11 +45,11 @@ module RailsGuides next end - if results.validity - print "." - else + if results.errors.length > 0 print "E" errors_on_guides[f] = results.errors + else + print "." end end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index f9294b6616..3afadc8cba 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 5.1.0.beta1 (February 23, 2017) ## + * Fix running multiple tests in one `rake` command e.g. `bin/rake test:models test:controllers` diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index ef9bbf3d7e..654c7bae57 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -57,7 +57,7 @@ can read more about Action Pack in its {README}[link:files/actionpack/README_rdo * The \README file created within your application. * {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html]. -* {Ruby on \Rails Tutorial}[http://www.railstutorial.org/book]. +* {Ruby on \Rails Tutorial}[https://www.railstutorial.org/book]. * {Ruby on \Rails Guides}[http://guides.rubyonrails.org]. * {The API Documentation}[http://api.rubyonrails.org]. diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index 0c0343114f..49267c2329 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -1,5 +1,5 @@ require "rdoc/task" -require "rails/api/generator" +require_relative "generator" module Rails module API diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 1a6aed7ce4..89f7b5991f 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -4,6 +4,7 @@ require "active_support/core_ext/object/blank" require "active_support/key_generator" require "active_support/message_verifier" require "rails/engine" +require "rails/secrets" module Rails # An Engine with the responsibility of coordinating the whole boot process. @@ -385,18 +386,7 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - yaml = config.paths["config/secrets"].first - - if File.exist?(yaml) - require "erb" - - all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} - shared_secrets = all_secrets["shared"] - env_secrets = all_secrets[Rails.env] - - secrets.merge!(shared_secrets.deep_symbolize_keys) if shared_secrets - secrets.merge!(env_secrets.deep_symbolize_keys) if env_secrets - end + secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env) # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 6102af3fff..4223c38146 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -2,6 +2,7 @@ require "fileutils" require "active_support/notifications" require "active_support/dependencies" require "active_support/descendants_tracker" +require "rails/secrets" module Rails class Application @@ -77,6 +78,11 @@ INFO initializer :bootstrap_hook, group: :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end + + initializer :set_secrets_root, group: :all do + Rails::Secrets.root = root + Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index b0d33f87a3..b0592151b7 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -13,7 +13,8 @@ module Rails :railties_order, :relative_url_root, :secret_key_base, :secret_token, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, + :read_encrypted_secrets attr_writer :log_level attr_reader :encoding, :api_only @@ -51,6 +52,7 @@ module Rails @debug_exception_response_format = nil @x = Custom.new @enable_dependency_loading = false + @read_encrypted_secrets = false end def encoding=(value) @@ -80,7 +82,7 @@ module Rails @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config/secrets.yml" + paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 13f3b90b6d..0d4e6dc5a1 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -27,15 +27,23 @@ module Rails end # Receives a namespace, arguments and the behavior to invoke the command. - def invoke(namespace, args = [], **config) - namespace = namespace.to_s - namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) - namespace = "version" if %w( -v --version ).include? namespace + def invoke(full_namespace, args = [], **config) + namespace = full_namespace = full_namespace.to_s - if command = find_by_namespace(namespace) - command.perform(namespace, args, config) + if char = namespace =~ /:(\w+)$/ + command_name, namespace = $1, namespace.slice(0, char) else - find_by_namespace("rake").perform(namespace, args, config) + command_name = namespace + end + + command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) + command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) + + command = find_by_namespace(namespace, command_name) + if command && command.all_commands[command_name] + command.perform(command_name, args, config) + else + find_by_namespace("rake").perform(full_namespace, args, config) end end @@ -52,8 +60,10 @@ module Rails # # Notice that "rails:commands:webrat" could be loaded as well, what # Rails looks for is the first and last parts of the namespace. - def find_by_namespace(name) # :nodoc: - lookups = [ name, "rails:#{name}" ] + def find_by_namespace(namespace, command_name = nil) # :nodoc: + lookups = [ namespace ] + lookups << "#{namespace}:#{command_name}" if command_name + lookups.concat lookups.map { |lookup| "rails:#{lookup}" } lookup(lookups) diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index 1435792536..db20c71861 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -56,7 +56,9 @@ module Rails end def perform(command, args, config) # :nodoc: - command = nil if Rails::Command::HELP_MAPPINGS.include?(args.first) + if Rails::Command::HELP_MAPPINGS.include?(args.first) + command, args = "help", [] + end dispatch(command, args.dup, nil, config) end @@ -111,7 +113,7 @@ module Rails # For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb` # would return `rails/test`. def default_command_root - path = File.expand_path(File.join("../commands", command_name), __dir__) + path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) path if File.exist?(path) end @@ -129,6 +131,10 @@ module Rails super end end + + def command_root_namespace + (namespace.split(":") - %w( rails )).first + end end def help diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb index aa8dab71b0..2718b453a8 100644 --- a/railties/lib/rails/commands/generate/generate_command.rb +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -4,6 +4,9 @@ module Rails module Command class GenerateCommand < Base # :nodoc: def help + require_application_and_environment! + load_generators + Rails::Generators.help self.class.command_name end diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE new file mode 100644 index 0000000000..4b7deb4e2a --- /dev/null +++ b/railties/lib/rails/commands/secrets/USAGE @@ -0,0 +1,52 @@ +=== Storing Encrypted Secrets in Source Control + +The Rails `secrets` commands helps encrypting secrets to slim a production +environment's `ENV` hash. It's also useful for atomic deploys: no need to +coordinate key changes to get everything working as the keys are shipped +with the code. + +=== Setup + +Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +and `config/secrets.yml.enc` files. + +The latter contains all the keys to be encrypted while the former holds the +encryption key. + +Don't lose the key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +secrets. +Don't commit the key! Add `config/secrets.yml.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to +manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start + + +The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: + + production: + secret_key_base: so-secret-very-hidden-wow + payment_processing_gateway_key: much-safe-very-gaedwey-wow + +But that's where the similarities between `secrets.yml` and `secrets.yml.enc` +end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and +be encrypted. + +A `shared:` top level key is also supported such that any keys there is merged +into the other environments. + +=== Editing Secrets + +After `bin/rails secrets:setup`, run `bin/rails secrets:edit`. + +That command opens a temporary file in `$EDITOR` with the decrypted contents of +`config/secrets.yml.enc` to edit the encrypted secrets. + +When the temporary file is next saved the contents are encrypted and written to +`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets +from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb new file mode 100644 index 0000000000..3ba8c0c85b --- /dev/null +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -0,0 +1,36 @@ +require "active_support" +require "rails/secrets" + +module Rails + module Command + class SecretsCommand < Rails::Command::Base # :nodoc: + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + + def setup + require "rails/generators" + require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + + Rails::Generators::EncryptedSecretsGenerator.start + end + + def edit + require_application_and_environment! + + Rails::Secrets.read_for_editing do |tmp_path| + puts "Waiting for secrets file to be saved. Abort with Ctrl-C." + system("\$EDITOR #{tmp_path}") + end + + puts "New secrets encrypted and saved." + rescue Interrupt + puts "Aborted changing encrypted secrets: nothing saved." + rescue Rails::Secrets::MissingKeyError => error + say error.message + end + end + end +end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index d58721f648..1fa27a3155 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -99,8 +99,9 @@ module Rails class_option :port, aliases: "-p", type: :numeric, desc: "Runs Rails on the specified port.", banner: :port, default: 3000 - class_option :binding, aliases: "-b", type: :string, default: "localhost", - desc: "Binds Rails to the specified IP.", banner: :IP + class_option :binding, aliases: "-b", type: :string, + desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", + banner: :IP class_option :config, aliases: "-c", type: :string, default: "config.ru", desc: "Uses a custom rackup configuration.", banner: :file class_option :daemon, aliases: "-d", type: :boolean, default: false, @@ -133,28 +134,64 @@ module Rails no_commands do def server_options { - server: @server, - log_stdout: @log_stdout, - Port: port, - Host: host, - DoNotReverseLookup: true, - config: options[:config], - environment: environment, - daemonize: options[:daemon], - pid: pid, - caching: options["dev-caching"], - restart_cmd: restart_command + user_supplied_options: user_supplied_options, + server: @server, + log_stdout: @log_stdout, + Port: port, + Host: host, + DoNotReverseLookup: true, + config: options[:config], + environment: environment, + daemonize: options[:daemon], + pid: pid, + caching: options["dev-caching"], + restart_cmd: restart_command } end end private + def user_supplied_options + @user_supplied_options ||= begin + # Convert incoming options array to a hash of flags + # ["-p", "3001", "-c", "foo"] # => {"-p" => true, "-c" => true} + user_flag = {} + @original_options.each_with_index { |command, i| user_flag[command] = true if i.even? } + + # Collect all options that the user has explicitly defined so we can + # differentiate them from defaults + user_supplied_options = [] + self.class.class_options.select do |key, option| + if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"] + name = option.name.to_sym + case name + when :port + name = :Port + when :binding + name = :Host + when :"dev-caching" + name = :caching + when :daemonize + name = :daemon + end + user_supplied_options << name + end + end + user_supplied_options << :Host if ENV["Host"] + user_supplied_options << :Port if ENV["PORT"] + user_supplied_options.uniq + end + end + def port ENV.fetch("PORT", options[:port]).to_i end def host - ENV.fetch("HOST", options[:binding]) + unless (default_host = options[:binding]) + default_host = environment == "development" ? "localhost" : "0.0.0.0" + end + ENV.fetch("HOST", default_host) end def environment diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 9c49e0655a..3174ffb0dc 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -8,7 +8,7 @@ module Rails MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 85f66cc416..3f1bf6a5bb 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -214,6 +214,7 @@ module Rails rails.map! { |n| n.sub(/^rails:/, "") } rails.delete("app") rails.delete("plugin") + rails.delete("encrypted_secrets") hidden_namespaces.each { |n| groups.delete(n.to_s) } diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 04f6341471..56e286f259 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -322,7 +322,7 @@ module Rails return [] unless options[:webpack] comment = "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" - GemfileEntry.github "webpacker", "rails/webpacker", nil, comment + GemfileEntry.new "webpacker", nil, comment end def jbuilder_gemfile_entry diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 86326d98ed..442258c9d1 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -32,6 +32,14 @@ module Rails # This allows you to override entire operations, like the creation of the # Gemfile, README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. + # + # class CustomAppBuilder < Rails::AppBuilder + # def test + # @generator.gem "rspec-rails", group: [:development, :test] + # run "bundle install" + # generate "rspec:install" + # end + # end class AppBuilder def rakefile template "Rakefile" diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 4a39e43e57..9c4a77fd1d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -14,6 +14,11 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true + # Attempt to read encrypted secrets from `config/secrets.yml.enc`. + # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or + # `config/secrets.yml.key`. + config.read_encrypted_secrets = true + # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index 8e995a5df1..816efcc5b1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -23,8 +23,10 @@ development: test: secret_key_base: <%= app_secret %> -# Do not keep production secrets in the repository, -# instead read values from the environment. +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. production: secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb new file mode 100644 index 0000000000..8b29213610 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb @@ -0,0 +1,66 @@ +require "rails/generators/base" +require "rails/secrets" + +module Rails + module Generators + class EncryptedSecretsGenerator < Base + def add_secrets_key_file + unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc") + key = Rails::Secrets.generate_key + + say "Adding config/secrets.yml.key to store the encryption key: #{key}" + say "" + say "Save this in a password manager your team can access." + say "" + say "If you lose the key, no one, including you, can access any encrypted secrets." + + say "" + create_file "config/secrets.yml.key", key + say "" + end + end + + def ignore_key_file + if File.exist?(".gitignore") + unless File.read(".gitignore").include?(key_ignore) + say "Ignoring config/secrets.yml.key so it won't end up in Git history:" + say "" + append_to_file ".gitignore", key_ignore + say "" + end + else + say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:" + say key_ignore, :on_green + say "" + end + end + + def add_encrypted_secrets_file + unless File.exist?("config/secrets.yml.enc") + say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." + say "" + + template "config/secrets.yml.enc" do |prefill| + say "" + say "For now the file contains this but it's been encrypted with the generated key:" + say "" + say prefill, :on_green + say "" + + Secrets.encrypt(prefill) + end + + say "You can edit encrypted secrets with `bin/rails secrets:edit`." + + say "Add this to your config/environments/production.rb:" + say "config.read_encrypted_secrets = true" + end + end + + private + def key_ignore + [ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n") + end + end + end +end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc new file mode 100644 index 0000000000..70426a66a5 --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc @@ -0,0 +1,3 @@ +# See `secrets.yml` for tips on generating suitable keys. +# production: +# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index be211e016d..3fcd8607f0 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -432,7 +432,7 @@ end end def inside_application? - rails_app_path && app_path =~ /^#{rails_app_path}/ + rails_app_path && destination_root.start_with?(rails_app_path.to_s) end def relative_path diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt index 35a9bf8c8b..8385e6a8a2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -1,10 +1,4 @@ -$: << File.expand_path(File.expand_path('../../test', __FILE__)) +$: << File.expand_path(File.expand_path("../../test", __FILE__)) -require 'bundler/setup' -require 'rails/test_unit/minitest_plugin' - -Rails::TestUnitReporter.executable = 'bin/test' - -Minitest.run_via = :rails - -require "active_support/testing/autorun" +require "bundler/setup" +require "rails/plugin/test" diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb new file mode 100644 index 0000000000..ff043b488e --- /dev/null +++ b/railties/lib/rails/plugin/test.rb @@ -0,0 +1,7 @@ +require "rails/test_unit/minitest_plugin" + +Rails::TestUnitReporter.executable = "bin/test" + +Minitest.run_via = :rails + +require "active_support/testing/autorun" diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb new file mode 100644 index 0000000000..a083914109 --- /dev/null +++ b/railties/lib/rails/secrets.rb @@ -0,0 +1,111 @@ +require "yaml" + +module Rails + # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 + class Secrets # :nodoc: + class MissingKeyError < RuntimeError + def initialize + super(<<-end_of_message.squish) + Missing encryption key to decrypt secrets with. + Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"] + end_of_message + end + end + + @read_encrypted_secrets = false + @root = File # Wonky, but ensures `join` uses the current directory. + + class << self + attr_writer :root + attr_accessor :read_encrypted_secrets + + def parse(paths, env:) + paths.each_with_object(Hash.new) do |path, all_secrets| + require "erb" + + secrets = YAML.load(ERB.new(preprocess(path)).result) || {} + all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] + all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] + end + end + + def generate_key + cipher = new_cipher + SecureRandom.hex(cipher.key_len)[0, cipher.key_len] + end + + def key + ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key + end + + def encrypt(text) + cipher(:encrypt, text) + end + + def decrypt(data) + cipher(:decrypt, data) + end + + def read + decrypt(IO.binread(path)) + end + + def write(contents) + IO.binwrite("#{path}.tmp", encrypt(contents)) + FileUtils.mv("#{path}.tmp", path) + end + + def read_for_editing + tmp_path = File.join(Dir.tmpdir, File.basename(path)) + IO.binwrite(tmp_path, read) + + yield tmp_path + + write(IO.binread(tmp_path)) + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + + private + def handle_missing_key + raise MissingKeyError + end + + def read_key_file + if File.exist?(key_path) + IO.binread(key_path).strip + end + end + + def key_path + @root.join("config", "secrets.yml.key") + end + + def path + @root.join("config", "secrets.yml.enc").to_s + end + + def preprocess(path) + if path.end_with?(".enc") + if @read_encrypted_secrets + decrypt(IO.binread(path)) + else + "" + end + else + IO.read(path) + end + end + + def new_cipher + OpenSSL::Cipher.new("aes-256-cbc") + end + + def cipher(mode, data) + cipher = new_cipher.public_send(mode) + cipher.key = key + cipher.update(data) << cipher.final + end + end + end +end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 09931c108a..8e290239bd 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -11,7 +11,7 @@ require "rails/generators/test_case" require "active_support/testing/autorun" -if defined?(Capbyara) +if defined?(Capybara) && defined?(Puma) require "action_dispatch/system_test_case" end @@ -49,7 +49,7 @@ class ActionDispatch::IntegrationTest end end -if defined? Capybara +if defined?(Capybara) && defined?(Puma) class ActionDispatch::SystemTestCase def before_setup # :nodoc: @routes = Rails.application.routes diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index d2ce14f594..ee0d697599 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -184,5 +184,12 @@ module ApplicationTests Rails::Command.send(:remove_const, "APP_PATH") end + + test "help does not show hidden namespaces" do + FileUtils.cd(rails_root) do + output = `bin/rails generate --help` + assert_no_match "active_record:migration", output + end + end end end diff --git a/railties/test/application/help_test.rb b/railties/test/application/help_test.rb new file mode 100644 index 0000000000..0c3fe8bfa3 --- /dev/null +++ b/railties/test/application/help_test.rb @@ -0,0 +1,23 @@ +require "isolation/abstract_unit" + +class HelpTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails help` } + assert_match "The most common rails commands are", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -h` } + assert_match "The most common rails commands are", output + end +end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index ee03d8b86c..e773b52dbb 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -70,16 +70,18 @@ module ApplicationTests end def test_run_units - skip "we no longer have the concept of unit tests. Just different directories..." create_test_file :models, "foo" create_test_file :helpers, "bar_helper" create_test_file :unit, "baz_unit" create_test_file :controllers, "foobar_controller" - run_test_units_command.tap do |output| - assert_match "FooTest", output - assert_match "BarHelperTest", output - assert_match "BazUnitTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:units`.tap do |output| + assert_match "FooTest", output + assert_match "BarHelperTest", output + assert_match "BazUnitTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end @@ -117,16 +119,18 @@ module ApplicationTests end def test_run_functionals - skip "we no longer have the concept of functional tests. Just different directories..." create_test_file :mailers, "foo_mailer" create_test_file :controllers, "bar_controller" create_test_file :functional, "baz_functional" create_test_file :models, "foo" - run_test_functionals_command.tap do |output| - assert_match "FooMailerTest", output - assert_match "BarControllerTest", output - assert_match "BazFunctionalTest", output - assert_match "3 runs, 3 assertions, 0 failures", output + + Dir.chdir(app_path) do + `bin/rails test:functionals`.tap do |output| + assert_match "FooMailerTest", output + assert_match "BarControllerTest", output + assert_match "BazFunctionalTest", output + assert_match "3 runs, 3 assertions, 0 failures", output + end end end diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb new file mode 100644 index 0000000000..6b419ae7ae --- /dev/null +++ b/railties/test/application/version_test.rb @@ -0,0 +1,24 @@ +require "isolation/abstract_unit" +require "rails/gem_version" + +class VersionTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = Dir.chdir(app_path) { `bin/rails version` } + assert_equal "Rails #{Rails.gem_version}\n", output + end + + test "short-cut alias works" do + output = Dir.chdir(app_path) { `bin/rails -v` } + assert_equal "Rails #{Rails.gem_version}\n", output + end +end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index e3dfc3e82b..d21a80982b 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -121,6 +121,32 @@ class Rails::ServerTest < ActiveSupport::TestCase end end + def test_host + with_rails_env "development" do + options = parse_arguments([]) + assert_equal "localhost", options[:Host] + end + + with_rails_env "production" do + options = parse_arguments([]) + assert_equal "0.0.0.0", options[:Host] + end + + with_rails_env "development" do + args = ["-b", "127.0.0.1"] + options = parse_arguments(args) + assert_equal "127.0.0.1", options[:Host] + end + end + + def test_records_user_supplied_options + server_options = parse_arguments(["-p", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + end + def test_default_options server = Rails::Server.new old_default_options = server.default_options diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 1ac2b4cde0..986afb6d2a 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -335,6 +335,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "config/environments/production.rb" do |content| assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + assert_match(/^ config\.read_encrypted_secrets = true/, content) end end diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb new file mode 100644 index 0000000000..747abf19ed --- /dev/null +++ b/railties/test/generators/encrypted_secrets_generator_test.rb @@ -0,0 +1,42 @@ +require "generators/generators_test_helper" +require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + +class EncryptedSecretsGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + def setup + super + cd destination_root + end + + def test_generates_key_file_and_encrypted_secrets_file + run_generator + + assert_file "config/secrets.yml.key", /[\w\d]+/ + + assert File.exist?("config/secrets.yml.enc") + assert_no_match(/production:\n# external_api_key: [\w\d]+/, IO.binread("config/secrets.yml.enc")) + assert_match(/production:\n# external_api_key: [\w\d]+/, Rails::Secrets.read) + end + + def test_appends_to_gitignore + FileUtils.touch(".gitignore") + + run_generator + + assert_file ".gitignore", /config\/secrets.yml.key/, /(?!config\/secrets.yml.enc)/ + end + + def test_warns_when_ignore_is_missing + assert_match(/Add this to your ignore file/i, run_generator) + end + + def test_doesnt_generate_a_new_key_file_if_already_opted_in_to_encrypted_secrets + FileUtils.mkdir("config") + File.open("config/secrets.yml.enc", "w") { |f| f.puts "already secrety" } + + run_generator + + assert_no_file "config/secrets.yml.key" + end +end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index eaf1199601..8ec096e5c6 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -536,6 +536,21 @@ class PluginGeneratorTest < Rails::Generators::TestCase FileUtils.rm gemfile_path end + def test_creating_plugin_only_specify_plugin_name_in_app_directory_adds_gemfile_entry + # simulate application existence + gemfile_path = "#{Rails.root}/Gemfile" + Object.const_set("APP_PATH", Rails.root) + FileUtils.touch gemfile_path + + FileUtils.cd(destination_root) + run_generator ["bukkits"] + + assert_file gemfile_path, /gem 'bukkits', path: 'bukkits'/ + ensure + Object.send(:remove_const, "APP_PATH") + FileUtils.rm gemfile_path + end + def test_skipping_gemfile_entry # simulate application existence gemfile_path = "#{Rails.root}/Gemfile" diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 1902eac862..924503a522 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -22,6 +22,7 @@ require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require "tmpdir" +require "rails/secrets" module TestHelpers module Paths diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb new file mode 100644 index 0000000000..36e42cf1f9 --- /dev/null +++ b/railties/test/secrets_test.rb @@ -0,0 +1,108 @@ +require "abstract_unit" +require "isolation/abstract_unit" +require "rails/generators" +require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" +require "rails/secrets" + +class Rails::SecretsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + + @old_read_encrypted_secrets, Rails::Secrets.read_encrypted_secrets = + Rails::Secrets.read_encrypted_secrets, true + end + + def teardown + Rails::Secrets.read_encrypted_secrets = @old_read_encrypted_secrets + + teardown_app + end + + test "setting read to false skips parsing" do + Rails::Secrets.read_encrypted_secrets = false + + Dir.chdir(app_path) do + assert_equal Hash.new, Rails::Secrets.parse(%w( config/secrets.yml.enc ), env: "production") + end + end + + test "raises when reading secrets without a key" do + run_secrets_generator do + FileUtils.rm("config/secrets.yml.key") + + assert_raises Rails::Secrets::MissingKeyError do + Rails::Secrets.key + end + end + end + + test "reading with ENV variable" do + run_secrets_generator do + begin + old_key = ENV["RAILS_MASTER_KEY"] + ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip + FileUtils.rm("config/secrets.yml.key") + + assert_match "production:\n# external_api_key", Rails::Secrets.read + ensure + ENV["RAILS_MASTER_KEY"] = old_key + end + end + end + + test "reading from key file" do + run_secrets_generator do + File.binwrite("config/secrets.yml.key", "How do I know you feel it?") + + assert_equal "How do I know you feel it?", Rails::Secrets.key + end + end + + test "editing" do + run_secrets_generator do + decrypted_path = nil + + Rails::Secrets.read_for_editing do |tmp_path| + decrypted_path = tmp_path + + assert_match(/production:\n# external_api_key/, File.read(tmp_path)) + + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + assert_not File.exist?(decrypted_path) + assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read + end + end + + test "merging secrets with encrypted precedence" do + run_secrets_generator do + File.write("config/secrets.yml", <<-end_of_secrets) + test: + yeah_yeah: lets-go-walking-down-this-empty-street + end_of_secrets + + Rails::Secrets.write(<<-end_of_secrets) + test: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + Rails.application.config.root = app_path + Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺 + assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah + end + end + + private + def run_secrets_generator + Dir.chdir(app_path) do + capture(:stdout) do + Rails::Generators::EncryptedSecretsGenerator.start + end + + yield + end + end +end diff --git a/tasks/release.rb b/tasks/release.rb index d1717cec52..8fb151ceb4 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,11 +1,25 @@ FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties ) +FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") } root = File.expand_path("../../", __FILE__) version = File.read("#{root}/RAILS_VERSION").strip tag = "v#{version}" +gem_version = Gem::Version.new(version) directory "pkg" +# This "npm-ifies" the current version number +# With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its +# versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively. + +# "5.0.1" --> "5.0.1" +# "5.0.1.1" --> "5.0.1-1" * +# "5.0.0.rc1" --> "5.0.0-rc1" +# +# * This makes it a prerelease. That's bad, but we haven't come up with +# a better solution at the moment. +npm_version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s } + (FRAMEWORKS + ["rails"]).each do |framework| namespace framework do gem = "pkg/#{framework}-#{version}.gem" @@ -43,6 +57,17 @@ directory "pkg" raise "Could not insert PRE in #{file}" unless $1 File.open(file, "w") { |f| f.write ruby } + + require "json" + if File.exist?("#{framework}/package.json") && JSON.parse(File.read("#{framework}/package.json"))["version"] != npm_version + Dir.chdir("#{framework}") do + if sh "which npm" + sh "npm version #{npm_version} --no-git-tag-version" + else + raise "You must have npm installed to release Rails." + end + end + end end task gem => %w(update_versions pkg) do @@ -61,38 +86,10 @@ directory "pkg" task push: :build do sh "gem push #{gem}" - # When running the release task we usually run build first to check that the gem works properly. - # NPM will refuse to publish or rebuild the gem if the version is changed when the Rails gem - # versions are changed. This then causes the gem push to fail. Because of this we need to update - # the version and publish at the same time. if File.exist?("#{framework}/package.json") Dir.chdir("#{framework}") do - # This "npm-ifies" the current version - # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its - # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively. - - # In essence, the code below runs through all "."s that appear in the version, - # and checks to see if their index in the version string is greater than or equal to 2, - # and if so, it will change the "." to a "-". - - # Sample version transformations: - # irb(main):001:0> version = "5.0.1.1" - # => "5.0.1.1" - # irb(main):002:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } - # => "5.0.1-1" - # irb(main):003:0> version = "5.0.0.rc1" - # => "5.0.0.rc1" - # irb(main):004:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s } - # => "5.0.0-rc1" - version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s } - - # Check if npm is installed, and raise an error if not - if sh "which npm" - sh "npm version #{version} --no-git-tag-version" - sh "npm publish" - else - raise "You must have npm installed to release Rails." - end + npm_tag = version =~ /[a-z]/ ? "pre" : "latest" + sh "npm publish --tag #{npm_tag}" end end end @@ -104,9 +101,11 @@ namespace :changelog do (FRAMEWORKS + ["guides"]).each do |fw| require "date" fname = File.join fw, "CHANGELOG.md" + current_contents = File.read(fname) - header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n* No changes.\n\n\n" - contents = header + File.read(fname) + header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n" + header << "* No changes.\n\n\n" if current_contents =~ /\A##/ + contents = header + current_contents File.open(fname, "wb") { |f| f.write contents } end end @@ -143,7 +142,7 @@ namespace :all do task push: FRAMEWORKS.map { |f| "#{f}:push" } + ["rails:push"] task :ensure_clean_state do - unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock'`.strip.empty? + unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock\\|package.json\\|version.rb'`.strip.empty? abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" end @@ -158,14 +157,16 @@ namespace :all do end task :commit do - File.open("pkg/commit_message.txt", "w") do |f| - f.puts "# Preparing for #{version} release\n" - f.puts - f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" - end + unless `git status -s`.strip.empty? + File.open("pkg/commit_message.txt", "w") do |f| + f.puts "# Preparing for #{version} release\n" + f.puts + f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" + end - sh "git add . && git commit --verbose --template=pkg/commit_message.txt" - rm_f "pkg/commit_message.txt" + sh "git add . && git commit --verbose --template=pkg/commit_message.txt" + rm_f "pkg/commit_message.txt" + end end task :tag do @@ -173,7 +174,74 @@ namespace :all do sh "git push --tags" end - task prep_release: %w(ensure_clean_state build) + task prep_release: %w(ensure_clean_state build bundle commit) + + task release: %w(prep_release tag push) +end + +task :announce do + Dir.chdir("pkg/") do + if gem_version.segments[2] == 0 || gem_version.segments[3].is_a?(Integer) + # Not major releases, and not security releases + raise "Only valid for patch releases" + end + + sums = "$ shasum -a 256 *-#{version}.gem\n" + `shasum -a 256 *-#{version}.gem` - task release: %w(ensure_clean_state build bundle commit tag push) + puts "Hi everyone," + puts + + puts "I am happy to announce that Rails #{version} has been released." + puts + + previous_version = gem_version.segments[0, 3] + previous_version[2] -= 1 + previous_version = previous_version.join(".") + + if version =~ /rc/ + require "date" + future_date = Date.today + 5 + future_date += 1 while future_date.saturday? || future_date.sunday? + + github_user = `git config github.user`.chomp + + puts <<MSG +If no regressions are found, expect the final release on #{future_date.strftime('%A, %B %-d, %Y')}. +If you find one, please open an [issue on GitHub](https://github.com/rails/rails/issues/new) +#{"and mention me (@#{github_user}) on it, " unless github_user.empty?}so that we can fix it before the final release. + +MSG + end + + puts <<MSG +## CHANGES since #{previous_version} + +To view the changes for each gem, please read the changelogs on GitHub: + +MSG + FRAMEWORKS.sort.each do |framework| + puts "* [#{FRAMEWORK_NAMES[framework]} CHANGELOG](https://github.com/rails/rails/blob/v#{version}/#{framework}/CHANGELOG.md)" + end + puts <<MSG + +*Full listing* + +To see the full list of changes, [check out all the commits on +GitHub](https://github.com/rails/rails/compare/v#{previous_version}...v#{version}). + +## SHA-1 + +If you'd like to verify that your gem is the same as the one I've uploaded, +please use these SHA-256 hashes. + +Here are the checksums for #{version}: + +``` +#{sums} +``` + +As always, huge thanks to the many contributors who helped with this release. + +MSG + end end diff --git a/version.rb b/version.rb index 9c49e0655a..3174ffb0dc 100644 --- a/version.rb +++ b/version.rb @@ -8,7 +8,7 @@ module Rails MAJOR = 5 MINOR = 1 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |