diff options
136 files changed, 2459 insertions, 564 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 7fd35d8241..139ee8013b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -23,7 +23,7 @@ checks: engines: rubocop: enabled: true - channel: rubocop-0-58 + channel: rubocop-0-60 ratings: paths: diff --git a/.github/issue_template.md b/.github/issue_template.md index 2ff6a271db..87e666a7dd 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,13 +1,12 @@ ### Steps to reproduce - -(Guidelines for creating a bug report are [available -here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) +<!-- (Guidelines for creating a bug report are [available +here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) --> ### Expected behavior -Tell us what should happen +<!-- Tell us what should happen --> ### Actual behavior -Tell us what happens instead +<!-- Tell us what happens instead --> ### System configuration **Rails version**: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a36687ec99..85c0e29ff0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,13 @@ ### Summary -Provide a general description of the code changes in your pull +<!-- Provide a general description of the code changes in your pull request... were there any bugs you had fixed? If so, mention them. If these bugs have open GitHub issues, be sure to tag them here as well, -to keep the conversation linked together. +to keep the conversation linked together. --> ### Other Information -If there's anything else that's important and relevant to your pull +<!-- If there's anything else that's important and relevant to your pull request, mention that information here. This could include benchmarks, or other information. @@ -18,4 +18,4 @@ Finally, if your pull request affects documentation or any non-code changes, guidelines for those changes are [available here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) -Thanks for contributing to Rails! +Thanks for contributing to Rails! --> diff --git a/.rubocop.yml b/.rubocop.yml index 615a229432..033f7adf49 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -133,9 +133,6 @@ Style/FrozenStringLiteralComment: Style/RedundantFreeze: Enabled: true - Exclude: - - 'actionpack/lib/action_dispatch/journey/router/utils.rb' - - 'activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb' # Use `foo {}` not `foo{}`. Layout/SpaceBeforeBlockBraces: @@ -104,8 +104,6 @@ group :test do platforms :mri do gem "stackprof" gem "byebug" - # FIXME: Remove this when thor 0.21 is release - gem "thor", git: "https://github.com/erikhuda/thor.git", ref: "006832ea32480618791f89bb7d9e67b22fc814b9" end gem "benchmark-ips" diff --git a/Gemfile.lock b/Gemfile.lock index 1321436814..f4f3e430ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,4 @@ GIT - remote: https://github.com/erikhuda/thor.git - revision: 006832ea32480618791f89bb7d9e67b22fc814b9 - ref: 006832ea32480618791f89bb7d9e67b22fc814b9 - specs: - thor (0.20.0) - -GIT remote: https://github.com/matthewd/rb-inotify.git revision: 856730aad4b285969e8dd621e44808a7c5af4242 branch: close-handling @@ -32,9 +25,9 @@ GIT GIT remote: https://github.com/rails/webpacker.git - revision: 48d9fd52c3e9637dbb21e551b48c84c0f2dbb2ed + revision: bb132d591da35095e3246082cba3d693f847e0b5 specs: - webpacker (4.0.0.pre.pre.2) + webpacker (4.0.0.pre.3) activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) @@ -105,16 +98,16 @@ PATH GEM remote: https://rubygems.org/ specs: - activerecord-jdbc-adapter (52.0-java) + activerecord-jdbc-adapter (52.1-java) activerecord (~> 5.2.0) - activerecord-jdbcmysql-adapter (52.0-java) - activerecord-jdbc-adapter (= 52.0) + activerecord-jdbcmysql-adapter (52.1-java) + activerecord-jdbc-adapter (= 52.1) jdbc-mysql (~> 5.1.36) - activerecord-jdbcpostgresql-adapter (52.0-java) - activerecord-jdbc-adapter (= 52.0) + activerecord-jdbcpostgresql-adapter (52.1-java) + activerecord-jdbc-adapter (= 52.1) jdbc-postgres (>= 9.4, < 43) - activerecord-jdbcsqlite3-adapter (52.0-java) - activerecord-jdbc-adapter (= 52.0) + activerecord-jdbcsqlite3-adapter (52.1-java) + activerecord-jdbc-adapter (= 52.1) jdbc-sqlite3 (~> 3.8, < 3.30) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) @@ -123,17 +116,17 @@ GEM io-like (~> 0.3.0) ast (2.4.0) aws-eventstream (1.0.1) - aws-partitions (1.102.0) - aws-sdk-core (3.25.0) + aws-partitions (1.111.0) + aws-sdk-core (3.37.0) aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.7.0) - aws-sdk-core (~> 3) + aws-sdk-kms (1.11.0) + aws-sdk-core (~> 3, >= 3.26.0) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.17.1) - aws-sdk-core (~> 3, >= 3.21.2) + aws-sdk-s3 (1.23.1) + aws-sdk-core (~> 3, >= 3.26.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) aws-sigv4 (1.0.3) @@ -173,38 +166,38 @@ GEM childprocess faraday selenium-webdriver - bootsnap (1.3.1) + bootsnap (1.3.2) msgpack (~> 1.0) - bootsnap (1.3.1-java) + bootsnap (1.3.2-java) msgpack (~> 1.0) builder (3.2.3) bunny (2.9.2) amq-protocol (~> 2.3.0) byebug (10.0.2) - capybara (3.7.1) + capybara (3.10.1) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - xpath (~> 3.1) + regexp_parser (~> 1.2) + xpath (~> 3.2) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) - chromedriver-helper (2.0.0) + chromedriver-helper (2.1.0) archive-zip (~> 0.10) nokogiri (~> 1.8) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.0.5) - concurrent-ruby (1.0.5-java) + concurrent-ruby (1.1.3) connection_pool (2.2.2) cookiejar (0.3.3) crass (1.0.4) curses (1.0.2) daemons (1.2.6) - dalli (2.7.8) + dalli (2.7.9) dante (0.2.0) declarative (0.0.10) declarative-option (0.1.0) @@ -228,7 +221,7 @@ GEM event_emitter (0.2.6) eventmachine (1.2.7) execjs (2.7.0) - faraday (0.15.2) + faraday (0.15.3) multipart-post (>= 1.2, < 3) faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) @@ -252,39 +245,39 @@ GEM raabro (~> 1.1) globalid (0.4.1) activesupport (>= 4.2.0) - google-api-client (0.23.8) + google-api-client (0.25.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.5, < 0.7.0) httpclient (>= 2.8.1, < 3.0) mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - signet (~> 0.9) - google-cloud-core (1.2.3) + signet (~> 0.10) + google-cloud-core (1.2.7) google-cloud-env (~> 1.0) - google-cloud-env (1.0.2) + google-cloud-env (1.0.5) faraday (~> 0.11) - google-cloud-storage (1.13.1) + google-cloud-storage (1.15.0) digest-crc (~> 0.4) google-api-client (~> 0.23) google-cloud-core (~> 1.2) googleauth (~> 0.6.2) - googleauth (0.6.6) + googleauth (0.6.7) faraday (~> 0.12) jwt (>= 1.4, < 3.0) - memoist (~> 0.12) + memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (~> 0.7) - hiredis (0.6.1) - hiredis (0.6.1-java) + hiredis (0.6.3) + hiredis (0.6.3-java) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (1.1.0) + i18n (1.1.1) concurrent-ruby (~> 1.0) - image_processing (1.6.0) + image_processing (1.7.1) mini_magick (~> 4.0) - ruby-vips (>= 2.0.11, < 3) + ruby-vips (>= 2.0.13, < 3) io-like (0.3.0) jaro_winkler (1.5.1) jaro_winkler (1.5.1-java) @@ -303,7 +296,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.2.2) + loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -311,7 +304,7 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) memoist (0.16.0) - method_source (0.9.0) + method_source (0.9.1) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) @@ -334,7 +327,7 @@ GEM msgpack (1.2.4-x86-mingw32) multi_json (1.13.1) multipart-post (2.0.0) - mustache (1.0.5) + mustache (1.1.0) mustermann (1.0.3) mysql2 (0.5.2) mysql2 (0.5.2-x64-mingw32) @@ -350,14 +343,14 @@ GEM mini_portile2 (~> 2.3.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) path_expander (1.0.3) pg (1.1.3) pg (1.1.3-x64-mingw32) pg (1.1.3-x86-mingw32) powerpack (0.1.2) - psych (3.0.2) + psych (3.0.3) public_suffix (3.0.3) puma (3.12.0) puma (3.12.0-java) @@ -367,10 +360,10 @@ GEM thor raabro (1.1.6) racc (1.4.14) - rack (2.0.5) + rack (2.0.6) rack-cache (1.8.0) rack (>= 0.4) - rack-protection (2.0.3) + rack-protection (2.0.4) rack rack-proxy (0.6.5) rack @@ -386,9 +379,10 @@ GEM rb-fsevent (0.10.3) rdoc (6.0.4) redcarpet (3.2.3) - redis (4.0.2) + redis (4.0.3) redis-namespace (1.6.0) redis (>= 3.0.4) + regexp_parser (1.2.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -405,14 +399,14 @@ GEM resque (~> 1.26) rufus-scheduler (~> 3.2) retriable (3.1.2) - rubocop (0.58.2) + rubocop (0.60.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) + unicode-display_width (~> 1.4.0) ruby-progressbar (1.10.0) ruby-vips (2.0.13) ffi (~> 1.9) @@ -420,7 +414,7 @@ GEM rubyzip (1.2.2) rufus-scheduler (3.5.2) fugit (~> 1.1, >= 1.1.5) - sass (3.5.7) + sass (3.7.2) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -433,26 +427,26 @@ GEM tilt (>= 1.1, < 3) sdoc (1.0.0) rdoc (>= 5.0) - selenium-webdriver (3.14.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) - sequel (5.12.0) + rubyzip (~> 1.2, >= 1.2.2) + sequel (5.14.0) serverengine (2.0.7) sigdump (~> 0.2.2) - sidekiq (5.2.1) + sidekiq (5.2.3) connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) sigdump (0.2.4) - signet (0.9.1) + signet (0.11.0) addressable (~> 2.3) faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sinatra (2.0.3) + sinatra (2.0.4) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.3) + rack-protection (= 2.0.4) tilt (~> 2.0) sneakers (2.7.0) bunny (~> 2.9.2) @@ -477,6 +471,7 @@ GEM daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) + thor (0.20.3) thread_safe (0.3.6) thread_safe (0.3.6-java) tilt (2.0.8) @@ -485,10 +480,10 @@ GEM turbolinks-source (5.2.0) tzinfo (1.2.5) thread_safe (~> 0.1) - tzinfo-data (1.2018.5) + tzinfo-data (1.2018.7) tzinfo (>= 1.0.0) uber (0.1.0) - uglifier (4.1.18) + uglifier (4.1.19) execjs (>= 0.3.0, < 3) unicode-display_width (1.4.0) useragent (0.16.10) @@ -504,7 +499,7 @@ GEM websocket-driver (0.7.0-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) - xpath (3.1.0) + xpath (3.2.0) nokogiri (~> 1.8) PLATFORMS @@ -569,7 +564,6 @@ DEPENDENCIES sqlite3 (~> 1.3.6) stackprof sucker_punch - thor! turbolinks (~> 5) tzinfo-data uglifier (>= 1.3.0) @@ -579,4 +573,4 @@ DEPENDENCIES websocket-client-simple! BUNDLED WITH - 1.16.6 + 1.17.1 @@ -45,7 +45,7 @@ or to generate the body of an email. In Rails, View generation is handled by [Ac [Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails. In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library to generate and send emails; [Active Job](activejob/README.md), a -framework for declaring jobs and making them run on a variety of queueing +framework for declaring jobs and making them run on a variety of queuing backends; [Action Cable](actioncable/README.md), a framework to integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud and local files to Rails applications; diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index 137fa64431..31c6fb41f2 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actioncable/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "nio4r", "~> 2.0" diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js index df60b1fc24..07151b9d25 100644 --- a/actioncable/app/assets/javascripts/action_cable.js +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -238,8 +238,8 @@ }, this.getPollInterval()); }; ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { - var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max; - var interval = 5 * Math.log(this.reconnectAttempts + 1); + var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier; + var interval = multiplier * Math.log(this.reconnectAttempts + 1); return Math.round(clamp(interval, min, max) * 1e3); }; ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { @@ -275,7 +275,8 @@ }(); ConnectionMonitor.pollInterval = { min: 3, - max: 30 + max: 30, + multiplier: 5 }; ConnectionMonitor.staleThreshold = 6; var Consumer = function() { diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js index 8d6ac1f682..cd1e4602d8 100644 --- a/actioncable/app/javascript/action_cable/connection_monitor.js +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -75,8 +75,8 @@ class ConnectionMonitor { } getPollInterval() { - const {min, max} = this.constructor.pollInterval - const interval = 5 * Math.log(this.reconnectAttempts + 1) + const {min, max, multiplier} = this.constructor.pollInterval + const interval = multiplier * Math.log(this.reconnectAttempts + 1) return Math.round(clamp(interval, min, max) * 1000) } @@ -117,7 +117,8 @@ class ConnectionMonitor { ConnectionMonitor.pollInterval = { min: 3, - max: 30 + max: 30, + multiplier: 5 } ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index db6fc7ee9c..35574a2c73 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,8 @@ +* Mails with multipart `format` blocks with implicit render now also check for + a template name in options hash instead of only using the action name. + + *Marcus Ilgner* + * Allow ActionMailer classes to configure the parameterized delivery job Example: ``` diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index f2fb160bdd..6f8d6d0699 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "actionview", version s.add_dependency "activejob", version diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 509d859ac3..2bb1078bb8 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -942,9 +942,7 @@ module ActionMailer def collect_responses(headers) if block_given? - collector = ActionMailer::Collector.new(lookup_context) { render(action_name) } - yield(collector) - collector.responses + collect_responses_from_block(headers, &Proc.new) elsif headers[:body] collect_responses_from_text(headers) else @@ -952,6 +950,13 @@ module ActionMailer end end + def collect_responses_from_block(headers) + templates_name = headers[:template_name] || action_name + collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) } + yield(collector) + collector.responses + end + def collect_responses_from_text(headers) [{ body: headers.delete(:body), diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index cbbee9fae8..86c0172772 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -544,6 +544,12 @@ class BaseTest < ActiveSupport::TestCase assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded) end + test "you can specify a different template for multipart render" do + mail = BaseMailer.implicit_different_template_with_block("explicit_multipart_templates").deliver + assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded) + assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded) + end + test "should raise if missing template in implicit render" do assert_raises ActionView::MissingTemplate do BaseMailer.implicit_different_template("missing_template").deliver_now diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index a3101207dc..c1bb48cc96 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -111,6 +111,13 @@ class BaseMailer < ActionMailer::Base mail(template_name: template_name) end + def implicit_different_template_with_block(template_name = "") + mail(template_name: template_name) do |format| + format.text + format.html + end + end + def explicit_different_template(template_name = "") mail do |format| format.text { render template: "#{mailer_name}/#{template_name}" } diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 5554d4e6b8..90cf989100 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,11 +1,28 @@ +* Allow rescue from parameter parse errors: + + ``` + rescue_from ActionDispatch::Http::Parameters::ParseError do + head :unauthorized + end + ``` + + *Gannon McGibbon*, *Josh Cheek* + +* Reset Capybara sessions if failed system test screenshot raising an exception. + + Reset Capybara sessions if `take_failed_screenshot` raise exception + in system test `after_teardown`. + + *Maxim Perepelitsa* + * Use request object for context if there's no controller There is no controller instance when using a redirect route or a mounted rack application so pass the request object as the context when resolving dynamic CSP sources in this scenario. - + Fixes #34200. - + *Andrew White* * Apply mapping to symbols returned from dynamic CSP sources @@ -14,7 +31,7 @@ would be converted to a string implicity, e.g: policy.default_src -> { :self } - + would generate the header: Content-Security-Policy: default-src self diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 1dc8abf746..ec56de18f1 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionpack/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "rack", "~> 2.0" diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a678377d4f..7361946de5 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -253,7 +253,10 @@ module ActionController # This will display the wrapped hash in the log file. request.filtered_parameters.merge! wrapped_filtered_hash end - super + ensure + # NOTE: Rescues all exceptions so they + # may be caught in ActionController::Rescue. + return super end private diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 9f8f178402..cbb772175c 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -41,6 +41,8 @@ module ActionDispatch # Returns a hash of parameters with all sensitive data replaced. def filtered_parameters @filtered_parameters ||= parameter_filter.filter(parameters) + rescue ActionDispatch::Http::Parameters::ParseError + @filtered_parameters = {} end # Returns a hash of request.env with all sensitive data replaced. diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index be129965d1..498b1e6695 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -7,6 +7,11 @@ module ActionDispatch module MimeNegotiation extend ActiveSupport::Concern + RESCUABLE_MIME_FORMAT_ERRORS = [ + ActionController::BadRequest, + ActionDispatch::Http::Parameters::ParseError, + ] + included do mattr_accessor :ignore_accept_header, default: false end @@ -59,7 +64,7 @@ module ActionDispatch fetch_header("action_dispatch.request.formats") do |k| params_readable = begin parameters[:format] - rescue ActionController::BadRequest + rescue *RESCUABLE_MIME_FORMAT_ERRORS false end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 8d7431fd6b..13d0963a33 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -111,13 +111,23 @@ module ActionDispatch begin strategy.call(raw_post) rescue # JSON or Ruby code block errors. - my_logger = logger || ActiveSupport::Logger.new($stderr) - my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" - + log_parse_error_once raise ParseError end end + def log_parse_error_once + @parse_error_logged ||= begin + parse_logger = logger || ActiveSupport::Logger.new($stderr) + parse_logger.debug <<~MSG.chomp + Error occurred while parsing request parameters. + Contents: + + #{raw_post} + MSG + end + end + def params_parsers ActionDispatch::Request.parameter_parsers end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7bc364d370..44f23940d3 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -383,9 +383,6 @@ module ActionDispatch end self.request_parameters = Request::Utils.normalize_encode_params(pr) end - rescue Http::Parameters::ParseError # one of the parse strategies blew up - self.request_parameters = Request::Utils.normalize_encode_params(super || {}) - raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 30af3ff930..89a164f968 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -15,9 +15,6 @@ require "action_dispatch/journey/path/pattern" module ActionDispatch module Journey # :nodoc: class Router # :nodoc: - class RoutingError < ::StandardError # :nodoc: - end - attr_accessor :routes def initialize(routes) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 99f3b4c2cd..06ce165f76 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1171,10 +1171,16 @@ module ActionDispatch end def actions + if @except + available_actions - Array(@except).map(&:to_sym) + else + available_actions + end + end + + def available_actions if @only Array(@only).map(&:to_sym) - elsif @except - default_actions - Array(@except).map(&:to_sym) else default_actions 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 e47d5020f4..600e9c733b 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 @@ -17,8 +17,11 @@ module ActionDispatch end def after_teardown - take_failed_screenshot - Capybara.reset_sessions! + begin + take_failed_screenshot + ensure + Capybara.reset_sessions! + end ensure super end diff --git a/actionpack/test/controller/params_parse_test.rb b/actionpack/test/controller/params_parse_test.rb new file mode 100644 index 0000000000..440ab06fd7 --- /dev/null +++ b/actionpack/test/controller/params_parse_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ParamsParseTest < ActionController::TestCase + class UsersController < ActionController::Base + def create + head :ok + end + end + + tests UsersController + + def test_parse_error_logged_once + log_output = capture_log_output do + post :create, body: "{", as: :json + end + assert_equal <<~LOG, log_output + Error occurred while parsing request parameters. + Contents: + + { + LOG + end + + private + + def capture_log_output + output = StringIO.new + request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output) + yield + output.string + end +end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 4ed79073e5..3c39373e55 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -70,6 +70,10 @@ class RescueController < ActionController::Base render plain: "io error" end + rescue_from ActionDispatch::Http::Parameters::ParseError do + render plain: "parse error", status: :bad_request + end + before_action(only: :before_action_raises) { raise "umm nice" } def before_action_raises @@ -130,6 +134,11 @@ class RescueController < ActionController::Base raise ResourceUnavailableToRescueAsString end + def arbitrary_action + params + render plain: "arbitrary action" + end + def missing_template end @@ -306,6 +315,23 @@ class RescueControllerTest < ActionController::TestCase get :exception_with_no_handler_for_wrapper assert_response :unprocessable_entity end + + test "can rescue a ParseError" do + capture_log_output do + post :arbitrary_action, body: "{", as: :json + end + assert_response :bad_request + assert_equal "parse error", response.body + end + + private + + def capture_log_output + output = StringIO.new + request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output) + yield + output.string + end end class RescueTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index d336b96eff..d2146f12a5 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -853,6 +853,28 @@ class ResourcesTest < ActionController::TestCase end end + def test_resource_has_show_action_but_does_not_have_destroy_action + with_routing do |set| + set.draw do + resources :products, only: [:show, :destroy], except: :destroy + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + end + end + + def test_singleton_resource_has_show_action_but_does_not_have_destroy_action + with_routing do |set| + set.draw do + resource :account, only: [:show, :destroy], except: :destroy + end + + assert_singleton_resource_allowed_routes("accounts", {}, :show, [:new, :create, :edit, :update, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:new, :create, :edit, :update, :destroy]) + end + end + def test_resource_has_only_create_action_and_named_route with_routing do |set| set.draw do diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index fd8bae0cdf..237a25ba4f 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,21 @@ +* Prevent `ActionView::TextHelper#word_wrap` from unexpectedly stripping white space from the _left_ side of lines. + + For example, given input like this: + + ``` + This is a paragraph with an initial indent, + followed by additional lines that are not indented, + and finally terminated with a blockquote: + "A pithy saying" + ``` + + Calling `word_wrap` should not trim the indents on the first and last lines. + + Fixes #34487 + + *Lyle Mullican* + + * Add allocations to template rendering instrumentation. Adds the allocations for template and partial rendering to the server output on render. @@ -11,7 +29,7 @@ *Eileen M. Uchitelle*, *Aaron Patterson* * Respect the `only_path` option passed to `url_for` when the options are passed in as an array - + Fixes #33237. *Joel Ambass* diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 49ee1a292b..5f1e746421 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -26,6 +26,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionview/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "builder", "~> 3.1" diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index a338d076e4..3d378dcb2f 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -259,7 +259,7 @@ module ActionView # # => Once\r\nupon\r\na\r\ntime def word_wrap(text, line_width: 80, break_sequence: "\n") text.split("\n").collect! do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line end * break_sequence end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index 2925489f5d..e961a770e6 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -361,6 +361,10 @@ class TextHelperTest < ActionView::TestCase assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", line_width: 15)) end + def test_word_wrap_with_leading_spaces + assert_equal(" This is a paragraph\nthat includes some\nindented lines:\n Like this sample\n blockquote", word_wrap(" This is a paragraph that includes some\nindented lines:\n Like this sample\n blockquote", line_width: 25)) + end + def test_word_wrap_does_not_modify_the_options_hash options = { line_width: 15 } passed_options = options.dup diff --git a/activejob/README.md b/activejob/README.md index d49fcfe3c2..a2a5289ab7 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -1,7 +1,7 @@ # Active Job -- Make work happen later Active Job is a framework for declaring jobs and making them run on a variety -of queueing backends. These jobs can be everything from regularly scheduled +of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really. @@ -20,7 +20,7 @@ switch between them without having to rewrite your jobs. ## Usage -To learn how to use your preferred queueing backend see its adapter +To learn how to use your preferred queuing backend see its adapter documentation at [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). @@ -39,7 +39,7 @@ end Enqueue a job like so: ```ruby -MyJob.perform_later record # Enqueue a job to be performed as soon as the queueing system is free. +MyJob.perform_later record # Enqueue a job to be performed as soon as the queuing system is free. ``` ```ruby @@ -82,9 +82,9 @@ This works with any class that mixes in GlobalID::Identification, which by default has been mixed into Active Record classes. -## Supported queueing systems +## Supported queuing systems -Active Job has built-in adapters for multiple queueing backends (Sidekiq, +Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index be6292f737..20b9d4ccdd 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.name = "activejob" s.version = version s.summary = "Job framework with pluggable queues." - s.description = "Declare job classes that can be run by a variety of queueing backends." + s.description = "Declare job classes that can be run by a variety of queuing backends." s.required_ruby_version = ">= 2.4.1" @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activejob/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "globalid", ">= 0.3.6" end diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index 31347194d2..fa58c50ed0 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -14,18 +14,18 @@ module ActiveJob end # Raised when an unsupported argument type is set as a job argument. We - # currently support NilClass, Integer, Float, String, TrueClass, FalseClass, - # BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record). + # currently support String, Integer, Float, NilClass, TrueClass, FalseClass, + # BigDecimal, Symbol, Date, Time, DateTime, ActiveSupport::TimeWithZone, + # ActiveSupport::Duration, Hash, ActiveSupport::HashWithIndifferentAccess, + # Array or GlobalID::Identification instances, although this can be extended + # by adding custom serializers. # Raised if you set the key for a Hash something else than a string or # a symbol. Also raised when trying to serialize an object which can't be - # identified with a Global ID - such as an unpersisted Active Record model. + # identified with a GlobalID - such as an unpersisted Active Record model. class SerializationError < ArgumentError; end module Arguments extend self - # :nodoc: - PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] - # Serializes a set of arguments. Intrinsic types that can safely be # serialized without mutation are returned as-is. Arrays/Hashes are # serialized element by element. All other types are serialized using @@ -47,6 +47,8 @@ module ActiveJob private # :nodoc: + PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ] + # :nodoc: GLOBALID_KEY = "_aj_globalid" # :nodoc: SYMBOL_KEYS_KEY = "_aj_symbol_keys" @@ -62,7 +64,7 @@ module ActiveJob OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym, WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym, ] - private_constant :RESERVED_KEYS + private_constant :PERMITTED_TYPES, :RESERVED_KEYS, :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY def serialize_argument(argument) case argument @@ -73,14 +75,14 @@ module ActiveJob when Array argument.map { |arg| serialize_argument(arg) } when ActiveSupport::HashWithIndifferentAccess - result = serialize_hash(argument) - result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true) - result + serialize_indifferent_hash(argument) when Hash symbol_keys = argument.each_key.grep(Symbol).map(&:to_s) result = serialize_hash(argument) result[SYMBOL_KEYS_KEY] = symbol_keys result + when -> (arg) { arg.respond_to?(:permitted?) } + serialize_indifferent_hash(argument.to_h) else Serializers.serialize(argument) end @@ -146,6 +148,12 @@ module ActiveJob end end + def serialize_indifferent_hash(indifferent_hash) + result = serialize_hash(indifferent_hash) + result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true) + result + end + def transform_symbol_keys(hash, symbol_keys) # NOTE: HashWithIndifferentAccess#transform_keys always # returns stringified keys with indifferent access diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index 95b1062701..ed41fac4b8 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -40,7 +40,7 @@ module ActiveJob #:nodoc: # Records that are passed in are serialized/deserialized using Global # ID. More information can be found in Arguments. # - # To enqueue a job to be performed as soon as the queueing system is free: + # To enqueue a job to be performed as soon as the queuing system is free: # # ProcessPhotoJob.perform_later(photo) # diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index 334b24fb3b..61317c7cfc 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -130,7 +130,7 @@ module ActiveJob set_callback(:enqueue, :after, *filters, &blk) end - # Defines a callback that will get called around the enqueueing + # Defines a callback that will get called around the enqueuing # of the job. # # class VideoProcessJob < ActiveJob::Base diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 62bb5861bb..698153636b 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -78,7 +78,7 @@ module ActiveJob end # Returns a hash with the job data that can safely be passed to the - # queueing adapter. + # queuing adapter. def serialize { "job_class" => self.class.name, diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 53cb98fc71..b5b9f23c00 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -9,10 +9,12 @@ module ActiveJob # Includes the +perform_later+ method for job initialization. module ClassMethods - # Push a job onto the queue. The arguments must be legal JSON types - # (+string+, +int+, +float+, +nil+, +true+, +false+, +hash+ or +array+) or - # GlobalID::Identification instances. Arbitrary Ruby objects - # are not supported. + # Push a job onto the queue. By default the arguments must be either String, + # Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal, Symbol, Date, + # Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::Duration, + # Hash, ActiveSupport::HashWithIndifferentAccess, Array or + # GlobalID::Identification instances, although this can be extended by adding + # custom serializers. # # Returns an instance of the job class queued with arguments available in # Job#arguments. diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb index f5a343311f..e96dbcd4c9 100644 --- a/activejob/lib/active_job/execution.rb +++ b/activejob/lib/active_job/execution.rb @@ -26,7 +26,7 @@ module ActiveJob end end - # Performs the job immediately. The job is not sent to the queueing adapter + # Performs the job immediately. The job is not sent to the queuing adapter # but directly executed by blocking the execution of others until it's finished. # # MyJob.new(*args).perform_now diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 3e3a474fbb..525e79e302 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -3,7 +3,7 @@ module ActiveJob # == Active Job adapters # - # Active Job has adapters for the following queueing backends: + # Active Job has adapters for the following queuing backends: # # * {Backburner}[https://github.com/nesquena/backburner] # * {Delayed Job}[https://github.com/collectiveidea/delayed_job] @@ -52,7 +52,7 @@ module ActiveJob # # No: The adapter will run jobs at the next opportunity and cannot use perform_later. # - # N/A: The adapter does not support queueing. + # N/A: The adapter does not support queuing. # # NOTE: # queue_classic supports job scheduling since version 3.1. @@ -74,7 +74,7 @@ module ActiveJob # # No: Does not allow the priority of jobs to be configured. # - # N/A: The adapter does not support queueing, and therefore sorting them. + # N/A: The adapter does not support queuing, and therefore sorting them. # # ==== Timeout # diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 8b2981926f..e4e14016d9 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -5,6 +5,7 @@ require "active_job/arguments" require "models/person" require "active_support/core_ext/hash/indifferent_access" require "jobs/kwargs_job" +require "support/stubs/strong_parameters" class ArgumentSerializationTest < ActiveSupport::TestCase setup do @@ -49,6 +50,15 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_arguments_roundtrip([a: 1, "b" => 2]) end + test "serialize a ActionController::Parameters" do + parameters = Parameters.new(a: 1) + + assert_equal( + { "a" => 1, "_aj_hash_with_indifferent_access" => true }, + ActiveJob::Arguments.serialize([parameters]).first + ) + end + test "serialize a hash" do symbol_key = { a: 1 } string_key = { "a" => 1 } diff --git a/activejob/test/support/stubs/strong_parameters.rb b/activejob/test/support/stubs/strong_parameters.rb new file mode 100644 index 0000000000..acba3a4504 --- /dev/null +++ b/activejob/test/support/stubs/strong_parameters.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Parameters + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access + end + + def permitted? + true + end + + def to_h + @parameters.to_h + end +end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 7be466dc4c..493fca698a 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -25,5 +25,8 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index f0e1242554..0d9e761b1e 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -141,7 +141,9 @@ module ActiveModel @mutations_from_database = nil end - def changes_applied # :nodoc: + # Clears dirty data and moves +changes+ to +previously_changed+ and + # +mutations_from_database+ to +mutations_before_last_save+ respectively. + def changes_applied unless defined?(@attributes) @previously_changed = changes end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index af94d52d45..9de6b609a3 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -327,11 +327,11 @@ module ActiveModel # person.errors.added? :name, :too_long # => false # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) + message = message.call if message.respond_to?(:call) + if message.is_a? Symbol - self.details[attribute.to_sym].map { |e| e[:error] }.include? message + details[attribute.to_sym].include? normalize_detail(message, options) else - message = message.call if message.respond_to?(:call) - message = normalize_message(attribute, message, options) self[attribute].include? message end end diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index e8ee18c00e..b37dad1c41 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -12,6 +12,10 @@ module ActiveModel :decimal end + def serialize(value) + cast(value) + end + def type_cast_for_schema(value) value.to_s.inspect end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 41ff6443fe..185b5a24ae 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -228,6 +228,16 @@ class ErrorsTest < ActiveModel::TestCase assert_not person.errors.added?(:name) end + test "added? returns false when checking for an error with an incorrect or missing option" do + person = Person.new + person.errors.add :name, :too_long, count: 25 + + assert person.errors.added? :name, :too_long, count: 25 + assert_not person.errors.added? :name, :too_long, count: 24 + assert_not person.errors.added? :name, :too_long + assert_not person.errors.added? :name, "is too long" + end + test "added? returns false when checking for an error by symbol and a different error with same message is present" do I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) person = Person.new diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 9d80bbe51f..2a237f86cf 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,30 @@ +* Add an `:if_not_exists` option to `create_table`. + + Example: + + create_table :posts, if_not_exists: true do |t| + t.string :title + end + + That would execute: + + CREATE TABLE IF NOT EXISTS posts ( + ... + ) + + If the table already exists, `if_not_exists: false` (the default) raises an + exception whereas `if_not_exists: true` does nothing. + + *fatkodima*, *Stefan Kanev* + +* Defining an Enum as a Hash with blank key, or as an Array with a blank value, now raises an `ArgumentError`. + + *Christophe Maximin* + +* Adds support for multiple databases to `rails db:schema:cache:dump` and `rails db:schema:cache:clear`. + + *Gannon McGibbon* + * `update_columns` now correctly raises `ActiveModel::MissingAttributeError` if the attribute does not exist. diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index a857d00c05..bcdd82052c 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -28,6 +28,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "activemodel", version end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 9d9e8a4110..2cb0a2a4df 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -39,7 +39,9 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE " + create_sql << "IF NOT EXISTS " if o.if_not_exists + create_sql << "#{quote_table_name(o.name)} " statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys @@ -119,6 +121,11 @@ module ActiveRecord sql end + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + " TEMPORARY" if o.temporary + end + def foreign_key_in_create(from_table, to_table, options) options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 70607fda5a..db489143af 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of @@ -256,15 +258,25 @@ module ActiveRecord class TableDefinition include ColumnMethods - attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment + attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys + attr_writer :indexes + deprecate :indexes= - def initialize(name, temporary = false, options = nil, as = nil, comment: nil) + def initialize( + name, + temporary: false, + if_not_exists: false, + options: nil, + as: nil, + comment: nil, + ** + ) @columns_hash = {} @indexes = [] @foreign_keys = [] @primary_keys = nil @temporary = temporary + @if_not_exists = if_not_exists @options = options @as = as @name = name 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 8a7020a799..38cfc3a241 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -205,6 +205,9 @@ module ActiveRecord # Set to true to drop the table before creating it. # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_not_exists</tt>] + # Set to true to avoid raising an error when the table already exists. + # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. @@ -287,8 +290,8 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, comment: nil, **options) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment + def create_table(table_name, **options) + td = create_table_definition(table_name, options) if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do @@ -317,7 +320,9 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment.present? + if table_comment = options[:comment].presence + change_table_comment(table_name, table_comment) + end td.columns.each do |column| change_column_comment(table_name, column.name, column.comment) if column.comment.present? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 564b226b39..0f2b1e85ff 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -137,7 +137,7 @@ module ActiveRecord record.committed! else # if not running callbacks, only adds the record to the parent transaction - record.add_to_transaction + connection.add_transaction_record(record) end end ensure diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e75202b0be..0895d06356 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -93,11 +93,11 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end - when Float - if value.infinite? || value.nan? - "'#{value}'" - else + when Numeric + if value.finite? super + else + "'#{value}'" end when OID::Array::Data _quote(encode_array(value)) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index 8e381a92cf..ceb8b40bd9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -23,6 +23,17 @@ module ActiveRecord end super end + + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY + # tables are already UNLOGGED. + if o.temporary + " TEMPORARY" + elsif o.unlogged + " UNLOGGED" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 206b855a18..dc4a0bb26e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -175,6 +175,13 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + attr_reader :unlogged + + def initialize(*) + super + @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables + end + private def integer_like_primary_key_type(type, options) if type == :bigint || options[:limit] == 8 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a11a786ec7..d2ed699ee2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -85,6 +85,19 @@ module ActiveRecord class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = "PostgreSQL" + ## + # :singleton-method: + # PostgreSQL allows the creation of "unlogged" tables, which do not record + # data in the PostgreSQL Write-Ahead Log. This can make the tables faster, + # but significantly increases the risk of data loss if the database + # crashes. As a result, this should not be used in production + # environments. If you would like all created tables to be unlogged in + # the test environment you can add the following line to your test.rb + # file: + # + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + class_attribute :create_unlogged_tables, default: false + NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", string: { name: "character varying" }, diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e0355a316b..3312c3de01 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -396,6 +396,12 @@ module ActiveRecord end private + # See https://www.sqlite.org/limits.html, + # the default value is 999 when not configured. + def bind_params_length + 999 + end + def check_version if sqlite_version < "3.8.0" raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 777cb8a402..3ce9aad5fc 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -107,7 +107,7 @@ module ActiveRecord # end def connected_to(database: nil, role: nil, &blk) if database && role - raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments." elsif database if database.is_a?(Hash) role, database = database.first diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 3d97e4e513..3a600835e1 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -218,6 +218,10 @@ module ActiveRecord MSG raise ArgumentError, error_message end + + if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?) + raise ArgumentError, "Enum label name must not be blank." + end end ENUM_CONFLICT_MESSAGE = \ diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 456689ec6d..33c4066b89 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -20,7 +20,7 @@ module ActiveRecord # Indicates whether to use a stable #cache_key method that is accompanied # by a changing version in the #cache_version method. # - # This is +false+, by default until Rails 6.0. + # This is +true+, by default on Rails 5.2 and above. class_attribute :cache_versioning, instance_writer: false, default: false end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1c7ceb4981..475baa7559 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -300,15 +300,22 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." task dump: :load_config do - conn = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache( + ActiveRecord::Base.connection, + filename, + ) + end end desc "Clears a db/schema_cache.yml file." task clear: :load_config do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - rm_f filename, verbose: false + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + rm_f filename, verbose: false + end end end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index a502713e56..e225628bae 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -125,6 +125,10 @@ module ActiveRecord raise ArgumentError, "Invalid argument for .where.not(), got nil." when Arel::Nodes::In Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::IsNotDistinctFrom + Arel::Nodes::IsDistinctFrom.new(node.left, node.right) + when Arel::Nodes::IsDistinctFrom + Arel::Nodes::IsNotDistinctFrom.new(node.left, node.right) when Arel::Nodes::Equality Arel::Nodes::NotEqual.new(node.left, node.right) when String diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 974d7a1c0a..27e401a756 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -313,6 +313,16 @@ module ActiveRecord ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) end + def cache_dump_filename(namespace) + filename = if namespace == "primary" + "schema_cache.yml" + else + "#{namespace}_schema_cache.yml" + end + + ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration, spec_name, env| load_schema(configuration, format, file, env, spec_name) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index c5d5fca672..fe3842b905 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -375,10 +375,6 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status - ensure - if @transaction_state && @transaction_state.committed? - clear_transaction_record_state - end end private diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb index 2aa85a977e..551d56c2ff 100644 --- a/activerecord/lib/arel/nodes/equality.rb +++ b/activerecord/lib/arel/nodes/equality.rb @@ -7,5 +7,12 @@ module Arel # :nodoc: all alias :operand1 :left alias :operand2 :right end + + %w{ + IsDistinctFrom + IsNotDistinctFrom + }.each do |name| + const_set name, Class.new(Equality) + end end end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index e83a6f162f..77502dd199 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -18,6 +18,14 @@ module Arel # :nodoc: all Nodes::Equality.new self, quoted_node(other) end + def is_not_distinct_from(other) + Nodes::IsNotDistinctFrom.new self, quoted_node(other) + end + + def is_distinct_from(other) + Nodes::IsDistinctFrom.new self, quoted_node(other) + end + def eq_any(others) grouping_any :eq, others end diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index 8f65d303ac..92d309453c 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -95,6 +95,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Or :binary alias :visit_Arel_Nodes_OuterJoin :binary alias :visit_Arel_Nodes_Regexp :binary diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index 9054f0159b..6389c875cb 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -195,6 +195,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_JoinSource :binary alias :visit_Arel_Nodes_LessThan :binary alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Matches :binary alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb index 0a06aef60b..73166054da 100644 --- a/activerecord/lib/arel/visitors/ibm_db.rb +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -10,6 +10,12 @@ module Arel # :nodoc: all collector = visit o.expr, collector collector << " ROWS ONLY" end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index d564e19089..fdd864b40d 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -12,6 +12,31 @@ module Arel # :nodoc: all private + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + right = o.right + + if right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector << "EXISTS (VALUES (" + collector = visit o.left, collector + collector << ") INTERSECT VALUES (" + collector = visit right, collector + collector << "))" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + end + def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) collector << "ROW_NUMBER() OVER (ORDER BY " inject_join(o.children, collector, ", ") << ") as _row_num" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 32f6705d04..dd77cfdf66 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -4,34 +4,6 @@ module Arel # :nodoc: all module Visitors class MySQL < Arel::Visitors::ToSql private - def visit_Arel_Nodes_Union(o, collector, suppress_parens = false) - unless suppress_parens - collector << "( " - end - - case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector - end - - collector << " UNION " - - case o.right - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.right, collector, true - else - visit o.right, collector - end - - if suppress_parens - collector - else - collector << " )" - end - end - def visit_Arel_Nodes_Bin(o, collector) collector << "BINARY " visit o.expr, collector @@ -65,6 +37,17 @@ module Arel # :nodoc: all collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " <=> " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb index 30a1529d46..f96bf65ee5 100644 --- a/activerecord/lib/arel/visitors/oracle.rb +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -148,6 +148,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb index 7061f06087..b092aa95e0 100644 --- a/activerecord/lib/arel/visitors/oracle12.rb +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -56,6 +56,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index c5110fa89c..920776b4dc 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -77,6 +77,18 @@ module Arel # :nodoc: all grouping_parentheses o, collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT DISTINCT FROM " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS DISTINCT FROM " + visit o.right, collector + end + # Used by Lateral visitor to enclose select queries in parentheses def grouping_parentheses(o, collector) if o.expr.is_a? Nodes::SelectStatement diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb index cb1d2424ad..af6f7e856a 100644 --- a/activerecord/lib/arel/visitors/sqlite.rb +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -22,6 +22,18 @@ module Arel # :nodoc: all def visit_Arel_Nodes_False(o, collector) collector << "0" end + + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT " + visit o.right, collector + end end end end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 8e56fb55a2..f9fe4404eb 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -268,13 +268,11 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_Union(o, collector) - collector << "( " - infix_value(o, collector, " UNION ") << " )" + infix_value_with_paren(o, collector, " UNION ") end def visit_Arel_Nodes_UnionAll(o, collector) - collector << "( " - infix_value(o, collector, " UNION ALL ") << " )" + infix_value_with_paren(o, collector, " UNION ALL ") end def visit_Arel_Nodes_Intersect(o, collector) @@ -643,6 +641,26 @@ module Arel # :nodoc: all end end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 0" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 1" + end + end + def visit_Arel_Nodes_NotEqual(o, collector) right = o.right @@ -845,6 +863,23 @@ module Arel # :nodoc: all visit o.right, collector end + def infix_value_with_paren(o, collector, value, suppress_parens = false) + collector << "( " unless suppress_parens + collector = if o.left.class == o.class + infix_value_with_paren(o.left, collector, value, true) + else + visit o.left, collector + end + collector << value + collector = if o.right.class == o.class + infix_value_with_paren(o.right, collector, value, true) + else + visit o.right, collector + end + collector << " )" unless suppress_parens + collector + end + def aggregate(name, o, collector) collector << "#{name}(" if o.distinct @@ -858,6 +893,19 @@ module Arel # :nodoc: all collector end end + + def is_distinct_from(o, collector) + collector << "CASE WHEN " + collector = visit o.left, collector + collector << " = " + collector = visit o.right, collector + collector << " OR (" + collector = visit o.left, collector + collector << " IS NULL AND " + collector = visit o.right, collector + collector << " IS NULL)" + collector << " THEN 0 ELSE 1 END" + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb new file mode 100644 index 0000000000..a02bae1453 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class UnloggedTablesTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + TABLE_NAME = "things" + LOGGED_FIELD = "relpersistence" + LOGGED_QUERY = "SELECT #{LOGGED_FIELD} FROM pg_class WHERE relname = '#{TABLE_NAME}'" + LOGGED = "p" + UNLOGGED = "u" + TEMPORARY = "t" + + class Thing < ActiveRecord::Base + self.table_name = TABLE_NAME + end + + def setup + @connection = ActiveRecord::Base.connection + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + teardown do + @connection.drop_table TABLE_NAME, if_exists: true + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + def test_logged_by_default + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_unlogged_in_test_environment_when_unlogged_setting_enabled + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], UNLOGGED + end + + def test_not_included_in_schema_dump + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_no_match(/unlogged/i, dump_table_schema(TABLE_NAME)) + end + + def test_not_changed_in_change_table + @connection.create_table(TABLE_NAME) do |t| + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.change_table(TABLE_NAME) do |t| + t.column :name, :string + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_gracefully_handles_temporary_tables + @connection.create_table(TABLE_NAME, temporary: true) do |t| + end + + # Temporary tables are already unlogged, though this query results in a + # different result ("t" vs. "u"). This test is really just checking that we + # didn't try to run `CREATE TEMPORARY UNLOGGED TABLE`, which would result in + # a PostgreSQL error. + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], TEMPORARY + end +end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 75e5aaed53..1aa0348879 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -37,7 +37,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_default assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth - assert_equal "$150.55", PostgresqlMoney.new.depth_before_type_cast + assert_equal "150.55", PostgresqlMoney.new.depth_before_type_cast end def test_money_values diff --git a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb new file mode 100644 index 0000000000..93a7dafebd --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +module ActiveRecord + module ConnectionAdapters + class SQLite3Adapter + class BindParameterTest < ActiveRecord::SQLite3TestCase + def test_too_many_binds + topics = Topic.where(id: (1..999).to_a << 2**63) + assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1..999).to_a << 2**63) + assert_equal 0, topics.count + end + end + end + end +end diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb index 7163cb34d3..2ddbec3266 100644 --- a/activerecord/test/cases/arel/visitors/ibm_db_test.rb +++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb @@ -29,6 +29,45 @@ module Arel sql = compile(stmt) sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb index b0b031cca3..b6c2dd6ae7 100644 --- a/activerecord/test/cases/arel/visitors/informix_test.rb +++ b/activerecord/test/cases/arel/visitors/informix_test.rb @@ -54,6 +54,45 @@ module Arel sql = compile(stmt) sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"' end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb index 340376c3d6..74f34b4dad 100644 --- a/activerecord/test/cases/arel/visitors/mssql_test.rb +++ b/activerecord/test/cases/arel/visitors/mssql_test.rb @@ -94,6 +94,45 @@ module Arel sql = compile(stmt) sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."name") INTERSECT VALUES ('Aaron Patterson')) + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb index 9d3bad8516..5f37587957 100644 --- a/activerecord/test/cases/arel/visitors/mysql_test.rb +++ b/activerecord/test/cases/arel/visitors/mysql_test.rb @@ -13,16 +13,6 @@ module Arel @visitor.accept(node, Collectors::SQLString.new).value end - it "squashes parenthesis on multiple unions" do - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new subnode, Arel.sql("topright") - assert_equal 1, compile(node).scan("(").length - - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new Arel.sql("topleft"), subnode - assert_equal 1, compile(node).scan("(").length - end - ### # :'( # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 @@ -75,6 +65,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" <=> 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" <=> NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ NOT "users"."name" <=> NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb index 83a2ee36ca..4ce5cab4db 100644 --- a/activerecord/test/cases/arel/visitors/oracle12_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb @@ -56,6 +56,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb index e1dfe40cf9..893edc7f74 100644 --- a/activerecord/test/cases/arel/visitors/oracle_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle_test.rb @@ -192,6 +192,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb index f7f2c76b6f..0f9efb20b4 100644 --- a/activerecord/test/cases/arel/visitors/postgres_test.rb +++ b/activerecord/test/cases/arel/visitors/postgres_test.rb @@ -276,6 +276,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS NOT DISTINCT FROM 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT DISTINCT FROM NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS DISTINCT FROM NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb index 6650b6ff3a..ee4e07a675 100644 --- a/activerecord/test/cases/arel/visitors/sqlite_test.rb +++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb @@ -6,7 +6,11 @@ module Arel module Visitors class SqliteTest < Arel::Spec before do - @visitor = SQLite.new Table.engine.connection_pool + @visitor = SQLite.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value end it "defaults limit to -1" do @@ -27,6 +31,45 @@ module Arel node = Nodes::False.new() assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index b6426a211e..4bfa799a96 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -155,6 +155,43 @@ module Arel end end + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end + it "should visit string subclass" do [ Class.new(String).new(":'("), @@ -480,6 +517,28 @@ module Arel end end + describe "Nodes::Union" do + it "squashes parenthesis on multiple unions" do + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new subnode, Arel.sql("topright") + assert_equal("( left UNION right UNION topright )", compile(node)) + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION left UNION right )", compile(node)) + end + end + + describe "Nodes::UnionAll" do + it "squashes parenthesis on multiple union alls" do + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new subnode, Arel.sql("topright") + assert_equal("( left UNION ALL right UNION ALL topright )", compile(node)) + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION ALL left UNION ALL right )", compile(node)) + end + end + describe "Nodes::NotIn" do it "should know how to visit" do node = @attr.not_in [1, 2, 3] diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index a520fb1303..a394430dfe 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -127,6 +127,9 @@ module ActiveRecord ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do handler = ActiveRecord::Base.connection_handler assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.config) end ensure ActiveRecord::Base.establish_connection(:arunit) @@ -136,25 +139,54 @@ module ActiveRecord def test_switching_connections_with_database_config_hash previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" - config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } ActiveRecord::Base.connected_to(database: { writing: config }) do handler = ActiveRecord::Base.connection_handler assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config, pool.spec.config) end ensure ActiveRecord::Base.establish_connection(:arunit) ENV["RAILS_ENV"] = previous_env end + def test_switching_connections_with_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to(database: :readonly, role: :writing) { } + end + assert_equal "connected_to can only accept a `database` or a `role` argument, but not both arguments.", error.message + end + + def test_switching_connections_without_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to { } + end + assert_equal "must provide a `database` or a `role`.", error.message + end + def test_switching_connections_with_database_symbol previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" - ActiveRecord::Base.connected_to(database: :arunit2) do + config = { + "default_env" => { + "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" }, + "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connected_to(database: :readonly) do handler = ActiveRecord::Base.connection_handler - assert_equal handler, ActiveRecord::Base.connection_handlers[:arunit2] + assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config["default_env"]["readonly"], pool.spec.config) end ensure + ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) ENV["RAILS_ENV"] = previous_env end @@ -189,6 +221,27 @@ module ActiveRecord ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) end + + def test_connects_to_returns_array_of_established_connections + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + result = ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_equal( + [ + ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary"), + ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + ], + result + ) + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end end def test_connection_pools diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b4593ccdf2..867ce7082b 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -274,6 +274,24 @@ class EnumTest < ActiveRecord::TestCase end assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: { "" => 1, "active" => 2 } + end + end + + assert_match(/Enum label name must not be blank/, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: ["active", ""] + end + end + + assert_match(/Enum label name must not be blank/, e.message) end test "reserved enum names" do diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e7778af55b..7b388ebc5e 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -56,7 +56,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase assert_equal "bar", record.foo end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.prepared_statements test "cleans up after prepared statement failure in a transaction" do with_two_connections do |original_connection, ddl_connection| record = @klass.create! bar: "bar" diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d060c8899..661163b4a1 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -127,6 +127,36 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 20131219224947, migrator.current_version end + def test_create_table_raises_if_already_exists + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_raise(ActiveRecord::StatementInvalid) do + connection.create_table :testings do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + + def test_create_table_with_if_not_exists_true + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_nothing_raised do + connection.create_table :testings, if_not_exists: true do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table # using a copy as we need the drop_table method to # continue to work for the ensure block of the test diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index 14db63890e..079e664ee4 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -24,8 +24,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -49,8 +51,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43122334, + big_bank_balance: BigDecimal("234000567.952344") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -64,4 +68,26 @@ class NumericDataTest < ActiveRecord::TestCase assert_kind_of BigDecimal, m1.big_bank_balance assert_equal BigDecimal("234000567.95"), m1.big_bank_balance end + + if current_adapter?(:PostgreSQLAdapter) + def test_numeric_fields_with_nan + m = NumericData.new( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN"), + world_population: 2**62, + my_house_population: 3 + ) + assert_predicate m.bank_balance, :nan? + assert_predicate m.big_bank_balance, :nan? + assert m.save + + m1 = NumericData.find_by( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN") + ) + + assert_predicate m1.bank_balance, :nan? + assert_predicate m1.big_bank_balance, :nan? + end + end end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index 8703d238a0..0b06cec40b 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -92,12 +92,16 @@ class ActiveRecord::Relation original = WhereClause.new([ table["id"].in([1, 2, 3]), table["id"].eq(1), + table["id"].is_not_distinct_from(1), + table["id"].is_distinct_from(2), "sql literal", random_object ]) expected = WhereClause.new([ table["id"].not_in([1, 2, 3]), table["id"].not_eq(1), + table["id"].is_distinct_from(1), + table["id"].is_not_distinct_from(2), Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")), Arel::Nodes::Not.new(random_object) ]) diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index c0be45eee7..aa6b7915a2 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -591,6 +591,17 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:before_commit, :after_commit], @topic.history end + def test_commit_run_transactions_callbacks_with_nested_transactions + @topic.transaction do + @topic.transaction(requires_new: true) do + @topic.content = "foo" + @topic.save! + @topic.class.connection.add_transaction_record(@topic) + end + end + assert_equal [:before_commit, :after_commit], @topic.history + end + def test_rollback_does_not_run_transactions_callbacks_without_enrollment @topic.transaction do @topic.content = "foo" diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec index cb1bb00a25..2c8816df25 100644 --- a/activestorage/activestorage.gemspec +++ b/activestorage/activestorage.gemspec @@ -25,6 +25,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activestorage/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "actionpack", version s.add_dependency "activerecord", version diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index aa695c98b2..448a2eeebb 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -27,6 +27,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "i18n", ">= 0.7", "< 2" s.add_dependency "tzinfo", "~> 1.1" s.add_dependency "minitest", "~> 5.1" diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index f8344912bb..a721187d2d 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -52,8 +52,12 @@ module ActiveSupport end class Event - attr_reader :name, :time, :transaction_id, :payload, :children - attr_accessor :end + attr_reader :name, :time, :end, :transaction_id, :payload, :children + + def self.clock_gettime_supported? # :nodoc: + defined?(Process::CLOCK_PROCESS_CPUTIME_ID) && + !Gem.win_platform? + end def initialize(name, start, ending, transaction_id, payload) @name = name @@ -83,6 +87,11 @@ module ActiveSupport @allocation_count_finish = now_allocations end + def end=(ending) + ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!) + @end = ending + end + # Returns the CPU time (in milliseconds) passed since the call to # +start!+ and the call to +finish!+ def cpu_time @@ -130,7 +139,7 @@ module ActiveSupport Process.clock_gettime(Process::CLOCK_MONOTONIC) end - if defined?(Process::CLOCK_PROCESS_CPUTIME_ID) + if clock_gettime_supported? def now_cpu Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) end diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb index 9c8dffa9d8..8de01eb19b 100644 --- a/activesupport/lib/active_support/testing/parallelization.rb +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "drb" -require "drb/unix" +require "drb/unix" unless Gem.win_platform? require "active_support/core_ext/module/attribute_accessors" module ActiveSupport @@ -15,6 +15,8 @@ module ActiveSupport end def record(reporter, result) + raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown) + reporter.synchronize do reporter.record(result) end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index f87099566b..5f4e3f3fd3 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -772,6 +772,16 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") } end + def test_normalize_non_unicode_string + # Fullwidth Latin Capital Letter A in Windows 31J + str = "\u{ff21}".encode(Encoding::Windows_31J) + assert_raise Encoding::CompatibilityError do + ActiveSupport::Deprecation.silence do + ActiveSupport::Multibyte::Unicode.normalize(str) + end + end + end + private def string_from_classes(classes) diff --git a/guides/assets/stylesheets/main.rtl.css b/guides/assets/stylesheets/main.rtl.css new file mode 100644 index 0000000000..ea31d6017c --- /dev/null +++ b/guides/assets/stylesheets/main.rtl.css @@ -0,0 +1,762 @@ +/* Guides.rubyonrails.org */ +/* Main.css */ +/* Created January 30, 2009 */ +/* Modified February 8, 2009 +--------------------------------------- */ + +/* General +--------------------------------------- */ + +.right {float: right; margin-left: 1em;} +.left {float: left; margin-right: 1em;} +@media screen and (max-width: 480px) { + .right, .left { float: none; } +} +.small {font-size: smaller;} +.large {font-size: larger;} +.hide {display: none;} + +ul, ol { margin: 0 1.5em 1.5em 1.5em; } + +ul { list-style-type: disc; } +ol { list-style-type: decimal; } + +dl { margin: 0 0 1.5em 0; } +dl dt { font-weight: bold; } +dd { margin-right: 1.5em;} + +pre, code { + font-size: 1em; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + line-height: 1.5; + margin: 1.5em 0; + overflow: auto; + color: #222; +} + +p code { + background: #eee; + border-radius: 2px; + padding: 1px 3px; +} + +pre, tt, code { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +abbr, acronym { border-bottom: 1px dotted #666; } +address { margin: 0 0 1.5em; font-style: italic; } +del { color:#666; } + +blockquote { margin: 1.5em; color: #666; font-style: italic; } +strong { font-weight: bold; } +em, dfn { font-style: italic; } +dfn { font-weight: bold; } +sup, sub { line-height: 0; } +p {margin: 0 0 1.5em;} + +label { font-weight: bold; } +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } + +input.text, input.title, +textarea, select { + margin:0.5em 0; + border:1px solid #bbb; +} + +table { + margin: 0 0 1.5em; + border: 2px solid #CCC; + background: #FFF; + border-collapse: collapse; +} + +table th, table td { + padding: 9px 10px; + border: 1px solid #CCC; + border-collapse: collapse; +} + +table th { + border-bottom: 2px solid #CCC; + background: #EEE; + font-weight: bold; +} + +img { + max-width: 100%; +} + + +/* Structure and Layout +--------------------------------------- */ + +body { + text-align: center; + font-family: Helvetica, Arial, sans-serif; + font-size: 87.5%; + line-height: 1.5em; + background: #fff; + color: #999; + direction: rtl; +} + +.wrapper { + text-align: right; + margin: 0 auto; + max-width: 960px; + padding: 0 1em; +} + +.red-button { + display: inline-block; + border-top: 1px solid rgba(255,255,255,.5); + background: #751913; + background: -webkit-gradient(linear, right top, right bottom, from(#c52f24), to(#751913)); + background: -webkit-linear-gradient(top, #c52f24, #751913); + background: -moz-linear-gradient(top, #c52f24, #751913); + background: -ms-linear-gradient(top, #c52f24, #751913); + background: -o-linear-gradient(top, #c52f24, #751913); + padding: 9px 18px; + -webkit-border-radius: 11px; + -moz-border-radius: 11px; + border-radius: 11px; + -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0; + -moz-box-shadow: rgba(0,0,0,1) 0 1px 0; + box-shadow: rgba(0,0,0,1) 0 1px 0; + text-shadow: rgba(0,0,0,.4) 0 1px 0; + color: white; + font-size: 15px; + font-family: Helvetica, Arial, Sans-Serif; + text-decoration: none; + vertical-align: middle; + cursor: pointer; +} +.red-button:active { + border-top: none; + padding-top: 10px; + background: -webkit-gradient(linear, right top, right bottom, from(#751913), to(#c52f24)); + background: -webkit-linear-gradient(top, #751913, #c52f24); + background: -moz-linear-gradient(top, #751913, #c52f24); + background: -ms-linear-gradient(top, #751913, #c52f24); + background: -o-linear-gradient(top, #751913, #c52f24); +} + +#topNav { + padding: 1em 0; + color: #565656; + background: #222; +} + +.s-hidden { + display: none; +} + +@media screen and (min-width: 1025px) { + .more-info-button { + display: none; + } + .more-info-links { + list-style: none; + display: inline; + margin: 0; + } + + .more-info { + display: inline-block; + } + .more-info:after { + content: " |"; + } + + .more-info:last-child:after { + content: ""; + } +} + +@media screen and (max-width: 1024px) { + #topNav .wrapper { text-align: center; } + .more-info-button { + position: relative; + z-index: 25; + } + + .more-info-label { + display: none; + } + + .more-info-container { + position: absolute; + top: .5em; + z-index: 20; + margin: 0 auto; + right: 0; + left: 0; + width: 20em; + } + + .more-info-links { + display: block; + list-style: none; + background-color: #c52f24; + border-radius: 5px; + padding-top: 5.25em; + border: 1px #980905 solid; + } + .more-info-links.s-hidden { + display: none; + } + .more-info { + padding: .75em; + border-top: 1px #980905 solid; + } + .more-info a, .more-info a:link, .more-info a:visited { + display: block; + color: white; + width: 100%; + height: 100%; + text-decoration: none; + text-transform: uppercase; + } +} + +#header { + background: #c52f24 url(../images/header_tile.gif) repeat-x; + color: #FFF; + padding: 1.5em 0; + z-index: 99; +} + +#feature { + background: #d5e9f6 url(../images/feature_tile.gif) repeat-x; + color: #333; + padding: 0.5em 0 1.5em; +} + +#container { + color: #333; + padding: 0.5em 0 1.5em 0; +} + +#mainCol { + max-width: 630px; + margin-right: 2em; +} + +#subCol { + position: absolute; + z-index: 0; + top: 21px; + left: 0; + background: #FFF; + padding: 1em 1.5em 1em 1.25em; + width: 17em; + font-size: 0.9285em; + line-height: 1.3846em; + margin-left: 1em; +} + + +@media screen and (max-width: 800px) { + #subCol { + position: static; + width: inherit; + margin-right: -1em; + margin-left: 0; + padding-left: 1.25em; + } +} + +#footer { + padding: 2em 0; + background: #222 url(../images/footer_tile.gif) repeat-x; +} +#footer .wrapper { + padding-right: 1em; + max-width: 960px; +} + +#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-right: 1em; max-width: 960px;} +#feature .wrapper {max-width: 640px; padding-left: 23em; position: relative; z-index: 0;} + +@media screen and (max-width: 960px) { + #container .wrapper { padding-left: 23em; } +} + +@media screen and (max-width: 800px) { + #feature .wrapper, #container .wrapper { padding-left: 0; } +} + +/* Links +--------------------------------------- */ + +a, a:link, a:visited { + color: #ee3f3f; + text-decoration: underline; +} + +#mainCol a, #subCol a, #feature a {color: #980905;} +#mainCol a code, #subCol a code, #feature a code {color: #980905;} + +#mainCol a.anchorlink, #mainCol a.anchorlink code {color: #333;} +#mainCol a.anchorlink { text-decoration: none; } +#mainCol a.anchorlink:hover { text-decoration: underline; } + +/* Navigation +--------------------------------------- */ + +.nav { + margin: 0; + padding: 0; + list-style: none; + float: left; + margin-top: 1.5em; + font-size: 1.2857em; +} + +.nav .nav-item {color: #FFF; text-decoration: none;} +.nav .nav-item:hover {text-decoration: underline;} + +.guides-index-large, .guides-index-small .guides-index-item { + padding: 0.5em 1.5em; + border-radius: 1em; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + background: #980905; + position: relative; + color: white; +} + +.guides-index .guides-index-item { + background: #980905 url(../images/nav_arrow.gif) no-repeat left top; + padding-left: 1em; + position: relative; + z-index: 15; + padding-bottom: 0.125em; +} + +.guides-index:hover .guides-index-item, .guides-index .guides-index-item:hover { + background-position: left -81px; + text-decoration: underline !important; +} + +@media screen and (min-width: 481px) { + .nav { + float: left; + margin-top: 1.5em; + font-size: 1.2857em; + } + .nav>li { + display: inline; + margin-right: 0.5em; + } + .guides-index.guides-index-small { + display: none; + } +} + +@media screen and (max-width: 480px) { + .nav { + float: none; + width: 100%; + text-align: center; + } + .nav .nav-item { + display: block; + margin: 0; + width: 100%; + background-color: #980905; + border: solid 1px #620c04; + border-top: 0; + padding: 15px 0; + text-align: center; + } + .nav .nav-item, .nav-item.guides-index-item { + text-transform: uppercase; + } + .nav .nav-item:first-child, .nav-item.guides-index-small { + border-top: solid 1px #620c04; + } + .guides-index.guides-index-small { + display: block; + margin-top: 1.5em; + } + .guides-index.guides-index-large { + display: none; + } + .guides-index-small .guides-index-item { + font: inherit; + padding-right: .75em; + font-size: .95em; + background-position: 96% 16px; + -webkit-appearance: none; + } + .guides-index-small .guides-index-item:hover{ + background-position: 96% -65px; + } +} + +#guides { + width: 37em; + display: block; + background: #980905; + border-radius: 1em; + color: #f1938c; + padding: 1.5em 2em; + position: absolute; + z-index: 10; + top: -0.25em; + left: 0; + padding-top: 2em; +} + +#guides.visible { + display: block !important; +} + +.guides-section dt, .guides-section dd { + font-weight: normal; + font-size: 0.722em; + margin: 0; + padding: 0; +} +.guides-section dt { + margin: 0.5em 0 0; + padding:0; +} +#guides a { + background: none !important; + color: #FFF; + text-decoration: none; +} +#guides a:hover { + text-decoration: underline; +} +.guides-section-container { + display: flex; + flex-direction: column; + flex-wrap: wrap; + width: 100%; + max-height: 35em; +} + +.guides-section { + min-width: 5em; + margin: 0 2em 0.5em 0; + flex: auto; + max-width: 12em; +} + +.guides-section dd { + line-height: 1.3; + margin-bottom: 0.5em; +} + +#guides hr { + display: block; + border: none; + height: 1px; + color: #f1938c; + background: #f1938c; +} + +/* Headings +--------------------------------------- */ + +h1 { + font-size: 2.5em; + line-height: 1em; + margin: 0.6em 0 .2em; + font-weight: bold; +} + +h2 { + font-size: 2.1428em; + line-height: 1em; + margin: 0.7em 0 .2333em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h2 { + font-size: 1.45em; + } +} + +h3 { + font-size: 1.7142em; + line-height: 1.286em; + margin: 0.875em 0 0.2916em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h3 { + font-size: 1.45em; + } +} + +h4 { + font-size: 1.2857em; + line-height: 1.2em; + margin: 1.6667em 0 .3887em; + font-weight: bold; +} + +h5 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: bold; +} + +h6 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: normal; +} + +.section { + padding-bottom: 0.25em; + border-bottom: 1px solid #999; +} + +/* Content +--------------------------------------- */ + +.pic { + margin: 0 2em 2em 0; +} + +#topNav strong {color: #999; margin-left: 0.5em;} +#topNav strong a {color: #FFF;} + +#header h1 { + float: right; + background: url(../images/rails_guides_logo_1x.png) no-repeat; + width: 297px; + text-indent: -9999em; + margin: 0; + padding: 0; +} + +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and ( min--moz-device-pixel-ratio: 2), +only screen and ( -o-min-device-pixel-ratio: 2/1), +only screen and ( min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + #header h1 { + background: url(../images/rails_guides_logo_2x.png) no-repeat; + background-size: 160%; + } +} + +@media screen and (max-width: 480px) { + #header h1 { + float: none; + } +} + +#header h1 a { + text-decoration: none; + display: block; + height: 77px; +} + +#feature p { + font-size: 1.2857em; + margin-bottom: 0.75em; +} + +@media screen and (max-width: 480px) { + #feature p { + font-size: 1em; + } +} + +#feature ul {margin-right: 0;} +#feature ul li { + list-style: none; + background: url(../images/check_bullet.gif) no-repeat right 0.5em; + padding: 0.5em 1.75em 0.5em 1.75em; + font-size: 1.1428em; + font-weight: bold; +} + +#mainCol dd, #subCol dd { + padding: 0.25em 0 1em; + border-bottom: 1px solid #CCC; + margin-bottom: 1em; + margin-right: 0; + /*padding-right: 28px;*/ + padding-right: 0; +} + +#mainCol dt, #subCol dt { + font-size: 1.2857em; + padding: 0.125em 0 0.25em 0; + margin-bottom: 0; +} + +@media screen and (max-width: 480px) { + #mainCol dt, #subCol dt { + font-size: 1em; + } +} + +#mainCol dd.work-in-progress, #subCol dd.work-in-progress { + background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-right: 0; + margin-top: 0.25em; +} + +#mainCol dd.kindle, #subCol dd.kindle { + background: #d5e9f6 url(../images/tab_info.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-right: 0; + margin-top: 0.25em; +} + +#mainCol div.warning, #subCol dd.warning { + background: #f9d9d8 url(../images/tab_red.gif) no-repeat left top; + border: none; + padding: 1.25em 1.25em 0.25em 48px; + margin-right: 0; + margin-top: 0.25em; +} + +#subCol .chapters {color: #980905;} +#subCol .chapters a {font-weight: bold;} +#subCol .chapters ul a {font-weight: normal;} +#subCol .chapters li {margin-bottom: 0.75em;} +#subCol h3.chapter {margin-top: 0.25em;} +#subCol h3.chapter img {vertical-align: text-bottom;} +#subCol .chapters ul {margin-right: 0; margin-top: 0.5em;} +#subCol .chapters ul li { + list-style: none; + padding: 0 1em 0 0; + background: url(../images/bullet.gif) no-repeat right 0.45em; + margin-right: 0; + font-size: 1em; + font-weight: normal; +} + +#subCol li ul, li ol { margin:0 1.5em; } + +div.code_container { + background: #EEE url(../images/tab_grey.gif) no-repeat right top; + padding: 0.25em 48px 0.5em 1em; +} + +.note { + background: #fff9d8 url(../images/tab_note.gif) no-repeat right top; + border: none; + padding: 1em 48px 0.25em 1em; + margin: 0.25em 0 1.5em 0; +} + +.info { + background: #d5e9f6 url(../images/tab_info.gif) no-repeat right top; + border: none; + padding: 1em 48px 0.25em 1em; + margin: 0.25em 0 1.5em 0; +} + +#mainCol div.todo { + background: #fff9d8 url(../images/tab_yellow.gif) no-repeat right top; + border: none; + padding: 1em 48px 0.25em 1em; + margin: 0.25em 0 1.5em 0; +} + +.note code, .info code, .todo code { + background: #fff; +} + +#mainCol ul li { + list-style:none; + background: url(../images/grey_bullet.gif) no-repeat right 0.5em; + padding-right: 1em; + margin-right: 0; +} + +#subCol .content { + font-size: 0.7857em; + line-height: 1.5em; +} + +#subCol .content li { + font-weight: normal; + background: none; + padding: 0 0 1em; + font-size: 1.1667em; +} + +/* Clearing +--------------------------------------- */ + +.clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +* html .clearfix {height: 1%;} +.clearfix {display: block;} + +/* Same bottom margin for special boxes than for regular paragraphs, this way +intermediate whitespace looks uniform. */ +div.code_container, div.important, div.caution, div.warning, div.note, div.info { + margin-bottom: 1.5em; +} + +/* Remove bottom margin of paragraphs in special boxes, otherwise they get a +spurious blank area below with the box background. */ +div.important p, div.caution p, div.warning p, div.note p, div.info p { + margin-bottom: 1em; +} + +/* Edge Badge +--------------------------------------- */ + +#edge-badge { + position: fixed; + right: 0px; + top: 0px; + z-index: 100; + border: none; +} + +/* Foundation v2.1.4 http://foundation.zurb.com */ +/* Artfully masterminded by ZURB */ + +/* Mobile */ +@media only screen and (max-width: 767px) { + table.responsive { margin-bottom: 0; } + + .pinned { position: absolute; right: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-left: 1px solid #ccc; border-right: 1px solid #ccc; } + .pinned table { border-left: none; border-right: none; width: 100%; } + .pinned table th, .pinned table td { white-space: nowrap; } + .pinned td:last-child { border-bottom: 0; } + + div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-left: 1px solid #ccc; } + div.table-wrapper div.scrollable table { margin-right: 35%; } + div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; } + + table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; } + table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; } + +} diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index f2d4d6f647..b24c0205d5 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -25,5 +25,6 @@ RailsGuides::Generator.new( all: env_flag["ALL"], only: env_value["ONLY"], kindle: env_flag["KINDLE"], - language: env_value["GUIDES_LANGUAGE"] + language: env_value["GUIDES_LANGUAGE"], + rtl: env_value["RTL"] ).generate diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index c83538ad48..3d8f54ab34 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -17,13 +17,14 @@ module RailsGuides class Generator GUIDES_RE = /\.(?:erb|md)\z/ - def initialize(edge:, version:, all:, only:, kindle:, language:) + def initialize(edge:, version:, all:, only:, kindle:, language:, rtl: false) @edge = edge @version = version @all = all @only = only @kindle = kindle @language = language + @rtl = rtl if @kindle check_for_kindlegen @@ -116,6 +117,11 @@ module RailsGuides def copy_assets FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir) + + if @rtl + FileUtils.rm(Dir.glob("#{@output_dir}/stylesheets/main.css")) + FileUtils.mv("#{@output_dir}/stylesheets/main.rtl.css", "#{@output_dir}/stylesheets/main.css") + end end def output_file_for(guide) @@ -198,7 +204,7 @@ module RailsGuides def check_fragment_identifiers(html, anchors) html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier| next if fragment_identifier == "mainCol" # in layout, jumps to some DIV - unless anchors.member?(fragment_identifier) + unless anchors.member?(CGI.unescape(fragment_identifier)) guess = anchors.min { |a, b| Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b) } diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index e6c0ae31a8..2f602c3e0a 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -242,9 +242,9 @@ WebNotificationsChannel.broadcast_to( ``` The `WebNotificationsChannel.broadcast_to` call places a message in the current -subscription adapter (by default `redis` for production and `async` for development and -test environments)'s pubsub queue under a separate broadcasting name for each user. -For a user with an ID of 1, the broadcasting name would be `web_notifications:1`. +subscription adapter's pubsub queue under a separate broadcasting name for each user. +The default pubsub queue for Action Cable is `redis` in production and `async` in development and +test environments. For a user with an ID of 1, the broadcasting name would be `web_notifications:1`. The channel has been instructed to stream everything that arrives at `web_notifications:1` directly to the client by invoking the `received` diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 4dc69ef911..39239852ca 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -289,7 +289,7 @@ style if the code inside your block is so short that it fits in a single line. For example, you could send metrics for every job enqueued: ```ruby -class ApplicationJob +class ApplicationJob < ActiveJob::Base before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" } end ``` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index c98f24d786..0fda7c5cfd 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -538,7 +538,8 @@ end If you want to be sure that an association is present, you'll need to test whether the associated object itself is present, and not the foreign key used -to map the association. +to map the association. This way, it is not only checked that the foreign key +is not empty but also that the referenced object exists. ```ruby class LineItem < ApplicationRecord diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index f75517c5ab..4f3e8b2cff 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1662,6 +1662,8 @@ Controls what happens to the associated objects when their owner is destroyed: * `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects +The `:destroy` and `:delete_all` options also affect the semantics of the `collection.delete` and `collection=` methods by causing them to destroy associated objects when they are removed from the collection. + ##### `:foreign_key` By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 321eee637f..67b097f2ae 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -302,7 +302,7 @@ class Product < ApplicationRecord end ``` -NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. +NOTE: Notice that in this example we used the `cache_key_with_version` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key_with_version` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. ### SQL Caching @@ -563,7 +563,7 @@ class ProductsController < ApplicationController # If the request is stale according to the given timestamp and etag value # (i.e. it needs to be processed again) then execute this block - if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key) + if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version) respond_to do |wants| # ... normal response processing end @@ -577,7 +577,7 @@ class ProductsController < ApplicationController end ``` -Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key_with_version` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 5fd3ad17de..bbebf97c3f 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -36,35 +36,35 @@ $ rails --help Usage: rails COMMAND [ARGS] The most common rails commands are: -generate Generate new code (short-cut alias: "g") -console Start the Rails console (short-cut alias: "c") -server Start the Rails server (short-cut alias: "s") -... + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + ... All commands can be run with -h (or --help) for more information. In addition to those commands, there are: -about List versions of all Rails ... -assets:clean[keep] Remove old compiled assets -assets:clobber Remove compiled assets -assets:environment Load asset compile environment -assets:precompile Compile all the assets ... -... -db:fixtures:load Loads fixtures into the ... -db:migrate Migrate the database ... -db:migrate:status Display status of migrations -db:rollback Rolls the schema back to ... -db:schema:cache:clear Clears a db/schema_cache.yml file -db:schema:cache:dump Creates a db/schema_cache.yml file -db:schema:dump Creates a db/schema.rb file ... -db:schema:load Loads a schema.rb file ... -db:seed Loads the seed data ... -db:structure:dump Dumps the database structure ... -db:structure:load Recreates the databases ... -db:version Retrieves the current schema ... -... -restart Restart app by touching ... -tmp:create Creates tmp directories ... + about List versions of all Rails ... + assets:clean[keep] Remove old compiled assets + assets:clobber Remove compiled assets + assets:environment Load asset compile environment + assets:precompile Compile all the assets ... + ... + db:fixtures:load Loads fixtures into the ... + db:migrate Migrate the database ... + db:migrate:status Display status of migrations + db:rollback Rolls the schema back to ... + db:schema:cache:clear Clears a db/schema_cache.yml file + db:schema:cache:dump Creates a db/schema_cache.yml file + db:schema:dump Creates a db/schema.rb file ... + db:schema:load Loads a schema.rb file ... + db:seed Loads the seed data ... + db:structure:dump Dumps the database structure ... + db:structure:load Recreates the databases ... + db:version Retrieves the current schema ... + ... + restart Restart app by touching ... + tmp:create Creates tmp directories ... ``` Let's create a simple Rails application to step through each of these commands in context. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index d03943ba4a..de6766e12e 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -379,6 +379,14 @@ The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`. +The PostgreSQL adapter adds one additional configuration option: + +* `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables` + controls whether database tables created should be "unlogged," which can speed + up performance but adds a risk of data loss if the database crashes. It is + highly recommended that you do not enable this in a production environment. + Defaults to `false` in all environments. + The SQLite3Adapter adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` @@ -723,7 +731,7 @@ There are a few configuration options available in Active Support: `config.active_job` provides the following configuration options: -* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). +* `config.active_job.queue_adapter` sets the adapter for the queuing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). ```ruby # Be sure to have the adapter's gem in your Gemfile diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index ed47a0de0f..709a5146e9 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -324,6 +324,26 @@ $ cd actionmailer $ bundle exec rake test ``` +#### For a Specific Directory + +If you want to run the tests located in a specific directory use the `TEST_DIR` +environment variable. For example, this will run the tests in the +`railties/test/generators` directory only: + +```bash +$ cd railties +$ TEST_DIR=generators bundle exec rake test +``` + +#### For a Specific File + +You can run the tests for a particular file by using: + +```bash +$ cd actionpack +$ bundle exec ruby -w -Itest test/template/form_helper_test.rb +``` + #### Running a Single Test You can run a single test through ruby. For instance: @@ -333,8 +353,7 @@ $ cd actionmailer $ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` -The `-n` option allows you to run a single method instead of the whole -file. +The `-n` option allows you to run a single method instead of the whole file. #### Running tests with a specific seed diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 07538a1cb7..b3baf726e3 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -8,8 +8,6 @@ This guide covers how to setup an environment for Ruby on Rails core development After reading this guide, you will know: * How to set up your machine for Rails development -* How to run specific groups of unit tests from the Rails test suite -* How the Active Record portion of the Rails test suite operates -------------------------------------------------------------------------------- @@ -43,195 +41,131 @@ $ git clone https://github.com/rails/rails.git $ cd rails ``` -### Set up and Run the Tests +### Install Additional Tools and Services -The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. +Some Rails tests depend on additional tools that you need to install before running those specific tests. -Install first SQLite3 and its development files for the `sqlite3` gem. On macOS -users are done with: +Here's the list of each gems' additional dependencies: -```bash -$ brew install sqlite3 -``` - -In Ubuntu you're done with just: - -```bash -$ sudo apt-get install sqlite3 libsqlite3-dev -``` - -If you are on Fedora or CentOS, you're done with - -```bash -$ sudo yum install libsqlite3x libsqlite3x-devel -``` - -If you are on Arch Linux, you will need to run: - -```bash -$ sudo pacman -S sqlite -``` - -For FreeBSD users, you're done with: - -```bash -# pkg install sqlite3 -``` +* Action Cable depends on Redis +* Active Record depends on SQLite3, MySQL and PostgreSQL +* Active Storage depends on Yarn (additionally Yarn depends on + [Node.js](https://nodejs.org/)), ImageMagick, FFmpeg, muPDF, and on macOS + also XQuartz and Poppler. +* Active Support depends on memcached and Redis +* Railties depend on a JavaScript runtime environment, such as having + [Node.js](https://nodejs.org/) installed. -Or compile the `databases/sqlite3` port. +Install all the services you need to properly test the full gem you'll be +making changes to. -Get a recent version of [Bundler](https://bundler.io/) +NOTE: Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](https://redis.io/download#installation). -```bash -$ gem install bundler -$ gem update bundler -``` +NOTE: Active Record tests _must_ pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against single adapter. -and run: +Below you can find instructions on how to install all of the additional +tools for different OSes. -```bash -$ bundle install --without db -``` - -This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. +#### macOS -NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running. +On macOS you can use [Homebrew](https://brew.sh/) to install all of the +additional tools. -You can use [Homebrew](https://brew.sh/) to install memcached on macOS: +To install all run: ```bash -$ brew install memcached +$ brew bundle ``` -On Ubuntu you can install it with apt-get: +You'll also need to start each of the installed services. To list all +available services run: ```bash -$ sudo apt-get install memcached +$ brew services list ``` -Or use yum on Fedora or CentOS: +You can then start each of the services one by one like this: ```bash -$ sudo yum install memcached +$ brew services start mysql ``` -If you are running on Arch Linux: - -```bash -$ sudo pacman -S memcached -``` - -For FreeBSD users, you're done with: - -```bash -# pkg install memcached -``` +Replace `mysql` with the name of the service you want to start. -Alternatively, you can compile the `databases/memcached` port. +#### Ubuntu -With the dependencies now installed, you can run the test suite with: +To install all run: ```bash -$ bundle exec rake test -``` - -You can also run tests for a specific component, like Action Pack, by going into its directory and executing the same command: +$ sudo apt-get update +$ sudo apt-get install sqlite3 libsqlite3-dev + mysql-server libmysqlclient-dev + postgresql postgresql-client postgresql-contrib libpq-dev + redis-server memcached imagemagick ffmpeg mupdf mupdf-tools -```bash -$ cd actionpack -$ bundle exec rake test +# Install Yarn +$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +$ sudo apt-get install yarn ``` -If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests in the `railties/test/generators` directory only: - -```bash -$ cd railties -$ TEST_DIR=generators bundle exec rake test -``` +#### Fedora or CentOS -You can run the tests for a particular file by using: +To install all run: ```bash -$ cd actionpack -$ bundle exec ruby -Itest test/template/form_helper_test.rb -``` - -Or, you can run a single test in a particular file: +$ sudo dnf install sqlite-devel sqlite-libs + mysql-server mysql-devel + postgresql-server postgresql-devel + redis memcached imagemagick ffmpeg mupdf -```bash -$ cd actionpack -$ bundle exec ruby -Itest path/to/test.rb -n test_name +# Install Yarn +# Use this command if you do not have Node.js installed +$ curl --silent --location https://rpm.nodesource.com/setup_8.x | sudo bash - +# If you have Node.js installed, use this command instead +$ curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo +$ sudo dnf install yarn ``` -### Railties Setup - -Some Railties tests depend on a JavaScript runtime environment, such as having [Node.js](https://nodejs.org/) installed. - -### Active Record Setup - -Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them. - -WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. - -#### Database Configuration - -The Active Record test suite requires a custom config file: `activerecord/test/config.yml`. An example is provided in `activerecord/test/config.example.yml` which can be copied and used as needed for your environment. +#### Arch Linux -#### MySQL and PostgreSQL - -To be able to run the suite for MySQL and PostgreSQL we need their gems. Install -first the servers, their client libraries, and their development files. - -On macOS, you can run: - -```bash -$ brew install mysql -$ brew install postgresql -``` - -Follow the instructions given by Homebrew to start these. - -On Ubuntu, just run: - -```bash -$ sudo apt-get install mysql-server libmysqlclient-dev -$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev -``` - -On Fedora or CentOS, just run: +To install all run: ```bash -$ sudo yum install mysql-server mysql-devel -$ sudo yum install postgresql-server postgresql-devel +$ sudo pacman -S sqlite + mariadb libmariadbclient mariadb-clients + postgresql postgresql-libs + redis memcached imagemagick ffmpeg mupdf mupdf-tools poppler + yarn +$ sudo systemctl start redis ``` -If you are running Arch Linux, MySQL isn't supported anymore so you will need to -use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)): +NOTE: If you are running Arch Linux, MySQL isn't supported anymore so you will need to +use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)). -```bash -$ sudo pacman -S mariadb libmariadbclient mariadb-clients -$ sudo pacman -S postgresql postgresql-libs -``` +#### FreeBSD -FreeBSD users will have to run the following: +To install all run: ```bash -# pkg install mysql56-client mysql56-server -# pkg install postgresql94-client postgresql94-server +# pkg install sqlite3 + mysql80-client mysql80-server + postgresql11-client postgresql11-server + memcached imagemagick ffmpeg mupdf + yarn +# portmaster databases/redis ``` -Or install them through ports (they are located under the `databases` folder). -If you run into troubles during the installation of MySQL, please see -[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html). +Or install everything through ports (these packages are located under the +`databases` folder). -After that, run: +NOTE: If you run into troubles during the installation of MySQL, please see +[the MySQL documentation](https://dev.mysql.com/doc/refman/8.0/en/freebsd-installation.html). -```bash -$ rm .bundle/config -$ bundle install -``` +### Database Configuration -First, we need to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file). +There are couple of additional steps required to configure database engines +required for running Active Record tests. In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases: @@ -247,13 +181,6 @@ mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* to 'rails'@'localhost'; ``` -and create the test databases: - -```bash -$ cd activerecord -$ bundle exec rake db:mysql:build -``` - PostgreSQL's authentication works differently. To setup the development environment with your development account, on Linux or BSD, you just have to run: @@ -267,21 +194,24 @@ and for macOS: $ createuser --superuser $USER ``` -Then, you need to create the test databases with: +Then, you need to create the test databases for both MySQL and PostgreSQL with: ```bash $ cd activerecord -$ bundle exec rake db:postgresql:build +$ bundle exec rake db:create ``` -It is possible to build databases for both PostgreSQL and MySQL with: +NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". + +You can also create test databases for each database engine separately: ```bash $ cd activerecord -$ bundle exec rake db:create +$ bundle exec rake db:mysql:build +$ bundle exec rake db:postgresql:build ``` -You can cleanup the databases using: +and you can drop the databases using: ```bash $ cd activerecord @@ -290,138 +220,40 @@ $ bundle exec rake db:drop NOTE: Using the Rake task to create the test databases ensures they have the correct character set and collation. -NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". - If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. -### Action Cable Setup - -Action Cable uses Redis as its default subscriptions adapter ([read more](action_cable_overview.html#broadcasting)). Thus, in order to have Action Cable's tests passing you need to install and have Redis running. - -#### Install Redis From Source +### Install JavaScript dependencies -Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](https://redis.io/download#installation). - -#### Install Redis From Package Manager - -On macOS, you can run: - -```bash -$ brew install redis -``` - -Follow the instructions given by Homebrew to start these. - -On Ubuntu, just run: - -```bash -$ sudo apt-get install redis-server -``` - -On Fedora or CentOS (requires EPEL enabled), just run: - -```bash -$ sudo yum install redis -``` - -If you are running Arch Linux, just run: - -```bash -$ sudo pacman -S redis -$ sudo systemctl start redis -``` - -FreeBSD users will have to run the following: - -```bash -# portmaster databases/redis -``` - -### Active Storage Setup - -When working on Active Storage, it is important to note that you need to -install its JavaScript dependencies while working on that section of the -codebase. In order to install these dependencies, it is necessary to -have Yarn, a Node.js package manager, available on your system. A -prerequisite for installing this package manager is that -[Node.js](https://nodejs.org) is installed. - - -On macOS, you can run: - -```bash -$ brew install yarn -``` - -On Ubuntu, you can run: - -```bash -$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - -$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - -$ sudo apt-get update && sudo apt-get install yarn -``` - -On Fedora or CentOS, just run: - -```bash -$ sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo - -$ sudo yum install yarn -``` - -Finally, after installing Yarn, you will need to run the following -command inside of the `activestorage` directory to install the dependencies: +If you installed Yarn, you will need to install the javascript dependencies: ```bash +$ cd activestorage $ yarn install ``` -Extracting previews, tested in Active Storage's test suite requires third-party -applications, ImageMagick for images, FFmpeg for video and muPDF for PDFs, and on macOS also XQuartz -and Poppler. Without these applications installed, Active Storage tests will -raise errors. +### Install Bundler gem -On macOS you can run: +Get a recent version of [Bundler](https://bundler.io/) ```bash -$ brew install ffmpeg -$ brew install imagemagick -$ brew cask install xquartz -$ brew install mupdf-tools -$ brew install poppler +$ gem install bundler +$ gem update bundler ``` -On Ubuntu, you can run: +and run: ```bash -$ sudo apt-get update -$ sudo apt-get install ffmpeg -$ sudo apt-get install imagemagick -$ sudo apt-get install mupdf mupdf-tools +$ bundle install ``` -On Fedora or CentOS, just run: +or: ```bash -$ sudo yum install ffmpeg -$ sudo yum install imagemagick -$ sudo yum install mupdf +$ bundle install --without db ``` -FreeBSD users can just run: +if you don't need to run Active Record tests. -```bash -# pkg install imagemagick -# pkg install ffmpeg -# pkg install mupdf -``` - -On Arch Linux, you can run: +### Contribute to Rails -```bash -$ sudo pacman -S ffmpeg -$ sudo pacman -S imagemagick -$ sudo pacman -S mupdf mupdf-tools -$ sudo pacman -S poppler -``` +After you've setup everything, read how you can start [contributing](contributing_to_ruby_on_rails.html#running-an-application-against-your-local-branch). diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index f5c0ba5b2d..4b56cf6296 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -107,8 +107,8 @@ HTML Guides ----------- Before generating the guides, make sure that you have the latest version of -Bundler installed on your system. As of this writing, you must install Bundler -1.3.5 or later on your device. +Bundler installed on your system. You can find the latest Bundler version +[here](https://rubygems.org/gems/bundler). As of this writing, it's v1.17.1. To install the latest version of Bundler, run `gem install bundler`. diff --git a/guides/source/security.md b/guides/source/security.md index bb996cc39c..dbec3cdd2d 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1235,6 +1235,11 @@ version: Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank ``` +Dependency Management and CVEs +------------------------------ + +We don’t bump dependencies just to encourage use of new versions, including for security issues. This is because application owners need to manually update their gems regardless of our efforts. Use `bundle update --conservative gem_name` to safely update vulnerable dependencies. + Additional Resources -------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index f94b67a0ac..448fd48f10 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,13 @@ +* Remove `app/assets` and `app/javascript` from `eager_load_paths` and `autoload_paths`. + + *Gannon McGibbon* + +* Add JSON support to rails properties route (`/rails/info/properties`). + + Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`. + + *Yoshiyuki Hirano* + * Use Ids instead of memory addresses when displaying references in scaffold views. Fixes #29200. diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 6d99ac9936..f09aa3ae0d 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -83,20 +83,21 @@ module Rails end def print_commands # :nodoc: - sorted_groups.each { |b, n| print_list(b, n) } + commands.each { |command| puts(" #{command}") } end - def sorted_groups # :nodoc: - lookup! + private + COMMANDS_IN_USAGE = %w(generate console server test test:system dbconsole new) + private_constant :COMMANDS_IN_USAGE - groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first } - groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort } + def commands + lookup! - rails = groups.delete("rails") - [[ "rails", rails ]] + groups.sort.to_a - end + visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands) + + (visible_commands - COMMANDS_IN_USAGE).sort + end - private def command_type # :doc: @command_type ||= "command" end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 6bf0406b21..4143b3c881 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -38,7 +38,9 @@ module Rails @paths ||= begin paths = Rails::Paths::Root.new(@root) - paths.add "app", eager_load: true, glob: "{*,*/concerns}" + paths.add "app", eager_load: true, + glob: "{*,*/concerns}", + exclude: %w(assets javascript) paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb" diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index f56f79b8d4..33002790d4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -258,8 +258,8 @@ module Rails class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, desc: "Don't run bundle install" - class_option :webpack, type: :string, default: nil, - desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join('/')})" + class_option :webpack, type: :string, aliases: "--webpacker", default: nil, + desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join(", ")})" class_option :skip_webpack_install, type: :boolean, default: false, desc: "Don't run Webpack install" diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index a18e03e7db..3f73bae3da 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -23,7 +23,7 @@ FileUtils.chdir APP_ROOT do # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt index d3bcaa5ec8..c517b0f96b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt @@ -11,6 +11,10 @@ # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https +<%- unless options[:skip_javascript] -%> +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? +<%- end -%> # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt index c918b57eca..c06cd525d7 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt @@ -4,7 +4,7 @@ require 'rails/test_help' class ActiveSupport::TestCase # Run tests in parallel with specified workers -<% if defined?(JRUBY_VERSION) -%> +<% if defined?(JRUBY_VERSION) || Gem.win_platform? -%> parallelize(workers: 2, with: :threads) <%- else -%> parallelize(workers: 2) diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index 3df36efc4c..b8173c8d11 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -54,6 +54,10 @@ module Rails table << "</table>" end end + + def to_json + Hash[properties].to_json + end end # The Rails version. diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index b4f4a5922a..50fe176946 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -14,8 +14,16 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def properties - @info = Rails::Info.to_html - @page_title = "Properties" + respond_to do |format| + format.html do + @info = Rails::Info.to_html + @page_title = "Properties" + end + + format.json do + render json: Rails::Info.to_json + end + end end def routes diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 87222563fd..8367ac8980 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -113,10 +113,11 @@ module Rails attr_accessor :glob def initialize(root, current, paths, options = {}) - @paths = paths - @current = current - @root = root - @glob = options[:glob] + @paths = paths + @current = current + @root = root + @glob = options[:glob] + @exclude = options[:exclude] options[:autoload_once] ? autoload_once! : skip_autoload_once! options[:eager_load] ? eager_load! : skip_eager_load! @@ -189,13 +190,11 @@ module Rails raise "You need to set a path root" unless @root.path result = [] - each do |p| - path = File.expand_path(p, @root.path) + each do |path| + path = File.expand_path(path, @root.path) if @glob && File.directory?(path) - Dir.chdir(path) do - result.concat(Dir.glob(@glob).map { |file| File.join path, file }.sort) - end + result.concat files_in(path) else result << path end @@ -222,6 +221,17 @@ module Rails end alias to_a expanded + + private + + def files_in(path) + Dir.chdir(path) do + files = Dir.glob(@glob) + files -= @exclude if @exclude + files.map! { |file| File.join(path, file) } + files.sort + end + end end end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 6fdb4648c2..4e4a504c97 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -30,6 +30,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/railties/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "actionpack", version diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index d02100d94c..a952d2466b 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -45,6 +45,8 @@ module ApplicationTests output.sub!(/^Resolving dependencies\.\.\.\n/, "") # Suppress Bundler platform warnings from output output.gsub!(/^The dependency .* will be unused .*\.\n/, "") + # Ignore warnings such as `Psych.safe_load is deprecated` + output.gsub!(/^warning:\s.*\n/, "") assert_equal(<<~OUTPUT, output) == Installing dependencies == diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index fa418f564b..b8a0434432 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1663,6 +1663,14 @@ module ApplicationTests assert_kind_of Hash, Rails.application.config.database_configuration end + test "autoload paths do not include asset paths" do + app "development" + ActiveSupport::Dependencies.autoload_paths.each do |path| + assert_not_operator path, :ends_with?, "app/assets" + assert_not_operator path, :ends_with?, "app/javascript" + end + end + test "raises with proper error message if no database configuration found" do FileUtils.rm("#{app_path}/config/database.yml") err = assert_raises RuntimeError do diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index 6478e06250..ef99365e75 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -65,6 +65,27 @@ module ApplicationTests end end + def db_migrate_and_schema_cache_dump + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + assert File.exist?("db/schema_cache.yml") + assert File.exist?("db/animals_schema_cache.yml") + end + end + + def db_migrate_and_schema_cache_dump_and_schema_cache_clear + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + rails "db:schema:cache:clear" + assert_not File.exist?("db/schema_cache.yml") + assert_not File.exist?("db/animals_schema_cache.yml") + end + end + def db_migrate_and_schema_dump_and_load(format) Dir.chdir(app_path) do generate_models_for_animals @@ -92,7 +113,7 @@ module ApplicationTests end end - def db_migrate_namespaced(namespace, expected_database) + def db_migrate_namespaced(namespace) Dir.chdir(app_path) do generate_models_for_animals output = rails("db:migrate:#{namespace}") @@ -104,7 +125,7 @@ module ApplicationTests end end - def db_migrate_status_namespaced(namespace, expected_database) + def db_migrate_status_namespaced(namespace) Dir.chdir(app_path) do generate_models_for_animals output = rails("db:migrate:status:#{namespace}") @@ -178,7 +199,7 @@ module ApplicationTests test "db:migrate:namespace works" do require "#{app_path}/config/environment" ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| - db_migrate_namespaced db_config.spec_name, db_config.config["database"] + db_migrate_namespaced db_config.spec_name end end @@ -190,10 +211,20 @@ module ApplicationTests test "db:migrate:status:namespace works" do require "#{app_path}/config/environment" ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| - db_migrate_namespaced db_config.spec_name, db_config.config["database"] - db_migrate_status_namespaced db_config.spec_name, db_config.config["database"] + db_migrate_namespaced db_config.spec_name + db_migrate_status_namespaced db_config.spec_name end end + + test "db:schema:cache:dump works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump + end + + test "db:schema:cache:clear works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump_and_schema_cache_clear + end end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index a0cdae898b..c38b91ef03 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -189,8 +189,10 @@ module ApplicationTests rails "generate", "model", "Product" rails "generate", "model", "Cart" rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to" - with_rails_env("test") { rails("db:migrate") } - rails("webpacker:compile") + with_rails_env("test") do + rails("db:migrate") + rails("webpacker:compile") + end output = rails("test") assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 5c34b205c9..140703e118 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -562,6 +562,44 @@ module ApplicationTests assert_no_match "create_table(:users)", output end + def test_run_in_parallel_with_unmarshable_exception + file = app_file "test/fail_test.rb", <<-RUBY + require "test_helper" + class FailTest < ActiveSupport::TestCase + class BadError < StandardError + def initialize + super + @proc = ->{ } + end + end + + test "fail" do + raise BadError + assert true + end + end + RUBY + + output = run_test_command(file) + + assert_match "DRb::DRbRemoteError: FailTest::BadError", output + assert_match "1 runs, 0 assertions, 0 failures, 1 errors", output + end + + def test_run_in_parallel_with_unknown_object + create_scaffold + app_file "config/environments/test.rb", <<-RUBY + Rails.application.configure do + config.action_controller.allow_forgery_protection = true + config.action_dispatch.show_exceptions = false + end + RUBY + + output = run_test_command("-n test_should_create_user") + + assert_match "ActionController::InvalidAuthenticityToken", output + end + def test_raise_error_when_specified_file_does_not_exist error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) } assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) @@ -703,6 +741,31 @@ module ApplicationTests end end + def test_reset_sessions_on_failed_system_test_screenshot + app_file "test/system/reset_sessions_on_failed_system_test_screenshot_test.rb", <<~RUBY + require "application_system_test_case" + + class ResetSessionsOnFailedSystemTestScreenshotTest < ApplicationSystemTestCase + ActionDispatch::SystemTestCase.class_eval do + def take_failed_screenshot + raise Capybara::CapybaraError + end + end + + Capybara.instance_eval do + def reset_sessions! + puts "Capybara.reset_sessions! called" + end + end + + test "dummy" do + end + end + RUBY + output = run_test_command("test/system/reset_sessions_on_failed_system_test_screenshot_test.rb") + assert_match "Capybara.reset_sessions! called", output + end + def test_system_tests_are_not_run_with_the_default_test_command app_file "test/system/dummy_test.rb", <<-RUBY require "application_system_test_case" diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 6d53230eab..a2b35124c5 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -308,7 +308,7 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_rails_should_run_rake_command_with_default_env + def test_rake_should_run_rake_command_with_default_env assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do with_rails_env nil do action :rake, "log:clear" @@ -316,13 +316,13 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_rails_with_env_option_should_run_rake_command_in_env + def test_rake_with_env_option_should_run_rake_command_in_env assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do action :rake, "log:clear", env: "production" end end - test "rails command with RAILS_ENV variable should run rake command in env" do + test "rake with RAILS_ENV variable should run rake command in env" do assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do with_rails_env "production" do action :rake, "log:clear" @@ -338,7 +338,7 @@ class ActionsTest < Rails::Generators::TestCase end end - test "rails command with sudo option should run rake command with sudo" do + test "rake with sudo option should run rake command with sudo" do assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do with_rails_env nil do action :rake, "log:clear", sudo: true diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index bb3aaa9d14..32d00f3157 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -230,6 +230,14 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_equal "false\n", output end + def test_csp_initializer_include_connect_src_example + run_generator + + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_match(/# policy\.connect_src/, content) + end + end + def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured app_root = File.join(destination_root, "myapp") run_generator [app_root] @@ -807,6 +815,9 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_no_gem "webpacker" + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_no_match(/policy\.connect_src/, content) + end end def test_webpack_option_with_js_framework diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 878a238f8d..6ab68f8333 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -50,6 +50,11 @@ class InfoControllerTest < ActionController::TestCase assert_select "table" end + test "info controller renders json with properties" do + get :properties, format: :json + assert_equal Rails::Info.to_json, response.body + end + test "info controller renders with routes" do get :routes assert_response :success diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 50522c1be6..d167a86e56 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -43,6 +43,18 @@ class InfoTest < ActiveSupport::TestCase end end + def test_json_includes_middleware + Rails::Info.module_eval do + property "Middleware", ["Rack::Lock", "Rack::Static"] + end + + hash = JSON.parse(Rails::Info.to_json) + assert_includes hash.keys, "Middleware" + properties.value_for("Middleware").each do |value| + assert_includes hash["Middleware"], value + end + end + private def properties Rails::Info.properties |